18da7d805SAndreas Gohr<?php 28da7d805SAndreas Gohr 38da7d805SAndreas Gohr/** 48da7d805SAndreas Gohr * @noinspection SqlNoDataSourceInspection 58da7d805SAndreas Gohr * @noinspection SqlDialectInspection 68da7d805SAndreas Gohr * @noinspection PhpComposerExtensionStubsInspection 78da7d805SAndreas Gohr */ 88da7d805SAndreas Gohr 98da7d805SAndreas Gohrnamespace dokuwiki\plugin\sqlite; 108da7d805SAndreas Gohr 11b35b734aSSzymon Olewniczakuse dokuwiki\Extension\Event; 120290deaeSAndreas Gohruse dokuwiki\Logger; 138da7d805SAndreas Gohr 148da7d805SAndreas Gohr/** 158da7d805SAndreas Gohr * Helpers to access a SQLite Database with automatic schema migration 168da7d805SAndreas Gohr */ 178da7d805SAndreas Gohrclass SQLiteDB 188da7d805SAndreas Gohr{ 19a7a40fb2SAnna Dabrowska public const FILE_EXTENSION = '.sqlite3'; 208da7d805SAndreas Gohr 218da7d805SAndreas Gohr /** @var \PDO */ 228da7d805SAndreas Gohr protected $pdo; 238da7d805SAndreas Gohr 248da7d805SAndreas Gohr /** @var string */ 258da7d805SAndreas Gohr protected $schemadir; 268da7d805SAndreas Gohr 278da7d805SAndreas Gohr /** @var string */ 288da7d805SAndreas Gohr protected $dbname; 298da7d805SAndreas Gohr 303a56750bSAndreas Gohr /** @var \helper_plugin_sqlite */ 318da7d805SAndreas Gohr protected $helper; 328da7d805SAndreas Gohr 33fe64ba38SAndreas Gohr 348da7d805SAndreas Gohr /** 358da7d805SAndreas Gohr * Constructor 368da7d805SAndreas Gohr * 378da7d805SAndreas Gohr * @param string $dbname Database name 388da7d805SAndreas Gohr * @param string $schemadir directory with schema migration files 398da7d805SAndreas Gohr * @param \helper_plugin_sqlite $sqlitehelper for backwards compatibility 408da7d805SAndreas Gohr * @throws \Exception 418da7d805SAndreas Gohr */ 428da7d805SAndreas Gohr public function __construct($dbname, $schemadir, $sqlitehelper = null) 438da7d805SAndreas Gohr { 44*48c6692fSAnna Dabrowska global $plugin_controller; 45*48c6692fSAnna Dabrowska if (!$plugin_controller->isEnabled('sqlite')) { 46*48c6692fSAnna Dabrowska throw new \Exception('SQLite plugin seems to be disabled.'); 47*48c6692fSAnna Dabrowska } 488da7d805SAndreas Gohr if (!class_exists('pdo') || !in_array('sqlite', \PDO::getAvailableDrivers())) { 498da7d805SAndreas Gohr throw new \Exception('SQLite PDO driver not available'); 508da7d805SAndreas Gohr } 518da7d805SAndreas Gohr 528da7d805SAndreas Gohr // backwards compatibility, circular dependency 538da7d805SAndreas Gohr $this->helper = $sqlitehelper; 543a56750bSAndreas Gohr if (!$this->helper) { 553a56750bSAndreas Gohr $this->helper = new \helper_plugin_sqlite(); 563a56750bSAndreas Gohr } 573a56750bSAndreas Gohr $this->helper->setAdapter($this); 588da7d805SAndreas Gohr 598da7d805SAndreas Gohr $this->schemadir = $schemadir; 608da7d805SAndreas Gohr $this->dbname = $dbname; 618da7d805SAndreas Gohr $file = $this->getDbFile(); 628da7d805SAndreas Gohr 638da7d805SAndreas Gohr $this->pdo = new \PDO( 648da7d805SAndreas Gohr 'sqlite:' . $file, 658da7d805SAndreas Gohr null, 668da7d805SAndreas Gohr null, 678da7d805SAndreas Gohr [ 6874c4ec25SAndreas Gohr \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, 6974c4ec25SAndreas Gohr \PDO::ATTR_TIMEOUT => 10, // wait for locks up to 10 seconds 708da7d805SAndreas Gohr ] 718da7d805SAndreas Gohr ); 728da7d805SAndreas Gohr 7374c4ec25SAndreas Gohr try { 7474c4ec25SAndreas Gohr // See https://www.sqlite.org/wal.html 7574c4ec25SAndreas Gohr $this->exec('PRAGMA journal_mode=WAL'); 7674c4ec25SAndreas Gohr } catch (\Exception $e) { 7774c4ec25SAndreas Gohr // this is not critical, but we log it as error. FIXME might be degraded to debug later 7874c4ec25SAndreas Gohr Logger::error('SQLite: Could not set WAL mode.', $e, $e->getFile(), $e->getLine()); 7974c4ec25SAndreas Gohr } 8074c4ec25SAndreas Gohr 818da7d805SAndreas Gohr if ($schemadir !== '') { 828da7d805SAndreas Gohr // schema dir is empty, when accessing the DB from Admin interface instead of plugin context 838da7d805SAndreas Gohr $this->applyMigrations(); 848da7d805SAndreas Gohr } 858da7d805SAndreas Gohr Functions::register($this->pdo); 868da7d805SAndreas Gohr } 878da7d805SAndreas Gohr 88aae177f9SAndreas Gohr /** 89549d6b89SAnna Dabrowska * Try optimizing the database before closing the connection. 90549d6b89SAnna Dabrowska * 91549d6b89SAnna Dabrowska * @see https://www.sqlite.org/pragma.html#pragma_optimize 92549d6b89SAnna Dabrowska */ 93549d6b89SAnna Dabrowska public function __destruct() 94549d6b89SAnna Dabrowska { 95549d6b89SAnna Dabrowska try { 96549d6b89SAnna Dabrowska $this->exec("PRAGMA analysis_limit=400"); 97549d6b89SAnna Dabrowska $this->exec('PRAGMA optimize;'); 98549d6b89SAnna Dabrowska } catch (\Exception $e) { 99549d6b89SAnna Dabrowska // ignore failures, this is not essential and not available until 3.18.0. 100549d6b89SAnna Dabrowska } 101549d6b89SAnna Dabrowska } 102549d6b89SAnna Dabrowska 103549d6b89SAnna Dabrowska /** 104aae177f9SAndreas Gohr * Do not serialize the DB connection 105aae177f9SAndreas Gohr * 106aae177f9SAndreas Gohr * @return array 107aae177f9SAndreas Gohr */ 108c9d29defSAndreas Gohr public function __sleep() 109c9d29defSAndreas Gohr { 110aae177f9SAndreas Gohr $this->pdo = null; 111aae177f9SAndreas Gohr return array_keys(get_object_vars($this)); 112aae177f9SAndreas Gohr } 113aae177f9SAndreas Gohr 114aae177f9SAndreas Gohr /** 115aae177f9SAndreas Gohr * On deserialization, reinit database connection 116aae177f9SAndreas Gohr */ 117c9d29defSAndreas Gohr public function __wakeup() 118c9d29defSAndreas Gohr { 119aae177f9SAndreas Gohr $this->__construct($this->dbname, $this->schemadir, $this->helper); 120aae177f9SAndreas Gohr } 1218da7d805SAndreas Gohr 1228da7d805SAndreas Gohr // region public API 1238da7d805SAndreas Gohr 1248da7d805SAndreas Gohr /** 1258da7d805SAndreas Gohr * Direct access to the PDO object 1268da7d805SAndreas Gohr * @return \PDO 1278da7d805SAndreas Gohr */ 12833e488b3SAndreas Gohr public function getPdo() 12933e488b3SAndreas Gohr { 130e22957e9SSzymon Olewniczak return $this->pdo; 131801b921eSSzymon Olewniczak } 132801b921eSSzymon Olewniczak 133801b921eSSzymon Olewniczak /** 1348da7d805SAndreas Gohr * Execute a statement and return it 1358da7d805SAndreas Gohr * 1368da7d805SAndreas Gohr * @param string $sql 13727eb38daSAndreas Gohr * @param ...mixed|array $parameters 1388da7d805SAndreas Gohr * @return \PDOStatement Be sure to close the cursor yourself 1398da7d805SAndreas Gohr * @throws \PDOException 1408da7d805SAndreas Gohr */ 14127eb38daSAndreas Gohr public function query($sql, ...$parameters) 1428da7d805SAndreas Gohr { 1430290deaeSAndreas Gohr $start = microtime(true); 1440290deaeSAndreas Gohr 14527eb38daSAndreas Gohr if ($parameters && is_array($parameters[0])) $parameters = $parameters[0]; 14627eb38daSAndreas Gohr 14703f14a77SAndreas Gohr // Statement preparation sometime throws ValueErrors instead of PDOExceptions, we streamline here 14803f14a77SAndreas Gohr try { 1498da7d805SAndreas Gohr $stmt = $this->pdo->prepare($sql); 15003f14a77SAndreas Gohr } catch (\Throwable $e) { 15103f14a77SAndreas Gohr throw new \PDOException($e->getMessage(), (int)$e->getCode(), $e); 15203f14a77SAndreas Gohr } 153b35b734aSSzymon Olewniczak $eventData = [ 154b8ae4891SSzymon Olewniczak 'sqlitedb' => $this, 155b35b734aSSzymon Olewniczak 'sql' => &$sql, 156b35b734aSSzymon Olewniczak 'parameters' => &$parameters, 157b35b734aSSzymon Olewniczak 'stmt' => $stmt 158b35b734aSSzymon Olewniczak ]; 159b35b734aSSzymon Olewniczak $event = new Event('PLUGIN_SQLITE_QUERY_EXECUTE', $eventData); 160b35b734aSSzymon Olewniczak if ($event->advise_before()) { 1618da7d805SAndreas Gohr $stmt->execute($parameters); 162b35b734aSSzymon Olewniczak } 163b35b734aSSzymon Olewniczak $event->advise_after(); 1640290deaeSAndreas Gohr 1650290deaeSAndreas Gohr $time = microtime(true) - $start; 1660290deaeSAndreas Gohr if ($time > 0.2) { 1670290deaeSAndreas Gohr Logger::debug('[sqlite] slow query: (' . $time . 's)', [ 1680290deaeSAndreas Gohr 'sql' => $sql, 1690290deaeSAndreas Gohr 'parameters' => $parameters, 1700290deaeSAndreas Gohr 'backtrace' => explode("\n", dbg_backtrace()) 1710290deaeSAndreas Gohr ]); 1720290deaeSAndreas Gohr } 1730290deaeSAndreas Gohr 1748da7d805SAndreas Gohr return $stmt; 1758da7d805SAndreas Gohr } 1768da7d805SAndreas Gohr 1778da7d805SAndreas Gohr /** 1788da7d805SAndreas Gohr * Execute a statement and return metadata 1798da7d805SAndreas Gohr * 1808da7d805SAndreas Gohr * Returns the last insert ID on INSERTs or the number of affected rows 1818da7d805SAndreas Gohr * 1828da7d805SAndreas Gohr * @param string $sql 18327eb38daSAndreas Gohr * @param ...mixed|array $parameters 1848da7d805SAndreas Gohr * @return int 1858da7d805SAndreas Gohr * @throws \PDOException 1868da7d805SAndreas Gohr */ 18727eb38daSAndreas Gohr public function exec($sql, ...$parameters) 1888da7d805SAndreas Gohr { 18927eb38daSAndreas Gohr $stmt = $this->query($sql, ...$parameters); 1908da7d805SAndreas Gohr 1918da7d805SAndreas Gohr $count = $stmt->rowCount(); 1928da7d805SAndreas Gohr $stmt->closeCursor(); 1938da7d805SAndreas Gohr if ($count && preg_match('/^INSERT /i', $sql)) { 1948da7d805SAndreas Gohr return $this->queryValue('SELECT last_insert_rowid()'); 1958da7d805SAndreas Gohr } 1968da7d805SAndreas Gohr 1978da7d805SAndreas Gohr return $count; 1988da7d805SAndreas Gohr } 1998da7d805SAndreas Gohr 2008da7d805SAndreas Gohr /** 2018da7d805SAndreas Gohr * Simple query abstraction 2028da7d805SAndreas Gohr * 2038da7d805SAndreas Gohr * Returns all data 2048da7d805SAndreas Gohr * 2058da7d805SAndreas Gohr * @param string $sql 20627eb38daSAndreas Gohr * @param ...mixed|array $params 2078da7d805SAndreas Gohr * @return array 2088da7d805SAndreas Gohr * @throws \PDOException 2098da7d805SAndreas Gohr */ 21027eb38daSAndreas Gohr public function queryAll($sql, ...$params) 2118da7d805SAndreas Gohr { 21227eb38daSAndreas Gohr $stmt = $this->query($sql, ...$params); 2138da7d805SAndreas Gohr $data = $stmt->fetchAll(\PDO::FETCH_ASSOC); 2148da7d805SAndreas Gohr $stmt->closeCursor(); 2158da7d805SAndreas Gohr return $data; 2168da7d805SAndreas Gohr } 2178da7d805SAndreas Gohr 2188da7d805SAndreas Gohr /** 2198da7d805SAndreas Gohr * Query one single row 2208da7d805SAndreas Gohr * 2218da7d805SAndreas Gohr * @param string $sql 22227eb38daSAndreas Gohr * @param ...mixed|array $params 2238da7d805SAndreas Gohr * @return array|null 2248da7d805SAndreas Gohr * @throws \PDOException 2258da7d805SAndreas Gohr */ 22627eb38daSAndreas Gohr public function queryRecord($sql, ...$params) 2278da7d805SAndreas Gohr { 22827eb38daSAndreas Gohr $stmt = $this->query($sql, ...$params); 229a7a36cdbSAndreas Gohr $row = $stmt->fetch(\PDO::FETCH_ASSOC); 2308da7d805SAndreas Gohr $stmt->closeCursor(); 231a7a36cdbSAndreas Gohr if (is_array($row) && count($row)) { 232a7a36cdbSAndreas Gohr return $row; 233a7a36cdbSAndreas Gohr } 2348da7d805SAndreas Gohr return null; 2358da7d805SAndreas Gohr } 2368da7d805SAndreas Gohr 2378da7d805SAndreas Gohr /** 2388da7d805SAndreas Gohr * Insert or replace the given data into the table 2398da7d805SAndreas Gohr * 2408da7d805SAndreas Gohr * @param string $table 2418da7d805SAndreas Gohr * @param array $data 2428da7d805SAndreas Gohr * @param bool $replace Conflict resolution, replace or ignore 243a7a36cdbSAndreas Gohr * @return array|null Either the inserted row or null if nothing was inserted 2448da7d805SAndreas Gohr * @throws \PDOException 2458da7d805SAndreas Gohr */ 2468da7d805SAndreas Gohr public function saveRecord($table, $data, $replace = true) 2478da7d805SAndreas Gohr { 2488da7d805SAndreas Gohr $columns = array_map(function ($column) { 2498da7d805SAndreas Gohr return '"' . $column . '"'; 2508da7d805SAndreas Gohr }, array_keys($data)); 2518da7d805SAndreas Gohr $values = array_values($data); 2528da7d805SAndreas Gohr $placeholders = array_pad([], count($columns), '?'); 2538da7d805SAndreas Gohr 2548da7d805SAndreas Gohr if ($replace) { 2558da7d805SAndreas Gohr $command = 'REPLACE'; 2568da7d805SAndreas Gohr } else { 2578da7d805SAndreas Gohr $command = 'INSERT OR IGNORE'; 2588da7d805SAndreas Gohr } 2598da7d805SAndreas Gohr 2608da7d805SAndreas Gohr /** @noinspection SqlResolve */ 261a7a40fb2SAnna Dabrowska $sql = $command . ' INTO "' . $table . '" (' . implode(',', $columns) . ') VALUES (' . implode( 262a7a40fb2SAnna Dabrowska ',', 263a7a40fb2SAnna Dabrowska $placeholders 264a7a40fb2SAnna Dabrowska ) . ')'; 265b35b734aSSzymon Olewniczak $stm = $this->query($sql, $values); 266a7a36cdbSAndreas Gohr $success = $stm->rowCount(); 2678da7d805SAndreas Gohr $stm->closeCursor(); 268a7a36cdbSAndreas Gohr 269a7a36cdbSAndreas Gohr if ($success) { 270a7a36cdbSAndreas Gohr $sql = 'SELECT * FROM "' . $table . '" WHERE rowid = last_insert_rowid()'; 271a7a36cdbSAndreas Gohr return $this->queryRecord($sql); 272a7a36cdbSAndreas Gohr } 273a7a36cdbSAndreas Gohr return null; 2748da7d805SAndreas Gohr } 2758da7d805SAndreas Gohr 2768da7d805SAndreas Gohr /** 2778da7d805SAndreas Gohr * Execute a query that returns a single value 2788da7d805SAndreas Gohr * 2798da7d805SAndreas Gohr * @param string $sql 28027eb38daSAndreas Gohr * @param ...mixed|array $params 2818da7d805SAndreas Gohr * @return mixed|null 2828da7d805SAndreas Gohr * @throws \PDOException 2838da7d805SAndreas Gohr */ 28427eb38daSAndreas Gohr public function queryValue($sql, ...$params) 2858da7d805SAndreas Gohr { 28627eb38daSAndreas Gohr $result = $this->queryAll($sql, ...$params); 287c9d29defSAndreas Gohr if (is_array($result) && count($result)) { 288c9d29defSAndreas Gohr return array_values($result[0])[0]; 289c9d29defSAndreas Gohr } 2908da7d805SAndreas Gohr return null; 2918da7d805SAndreas Gohr } 2928da7d805SAndreas Gohr 29333e488b3SAndreas Gohr /** 29433e488b3SAndreas Gohr * Execute a query that returns a list of key-value pairs 29533e488b3SAndreas Gohr * 29633e488b3SAndreas Gohr * The first column is used as key, the second as value. Any additional colums are ignored. 29733e488b3SAndreas Gohr * 29833e488b3SAndreas Gohr * @param string $sql 29927eb38daSAndreas Gohr * @param ...mixed|array $params 30033e488b3SAndreas Gohr * @return array 30133e488b3SAndreas Gohr */ 30227eb38daSAndreas Gohr public function queryKeyValueList($sql, ...$params) 30333e488b3SAndreas Gohr { 30427eb38daSAndreas Gohr $result = $this->queryAll($sql, ...$params); 30533e488b3SAndreas Gohr if (!$result) return []; 30633e488b3SAndreas Gohr if (count(array_keys($result[0])) != 2) { 30733e488b3SAndreas Gohr throw new \RuntimeException('queryKeyValueList expects a query that returns exactly two columns'); 30833e488b3SAndreas Gohr } 30933e488b3SAndreas Gohr [$key, $val] = array_keys($result[0]); 31033e488b3SAndreas Gohr 31133e488b3SAndreas Gohr return array_combine( 31233e488b3SAndreas Gohr array_column($result, $key), 31333e488b3SAndreas Gohr array_column($result, $val) 31433e488b3SAndreas Gohr ); 31533e488b3SAndreas Gohr } 31633e488b3SAndreas Gohr 3178da7d805SAndreas Gohr // endregion 3188da7d805SAndreas Gohr 3198da7d805SAndreas Gohr // region meta handling 3208da7d805SAndreas Gohr 3218da7d805SAndreas Gohr /** 3228da7d805SAndreas Gohr * Get a config value from the opt table 3238da7d805SAndreas Gohr * 3248da7d805SAndreas Gohr * @param string $opt Config name 3258da7d805SAndreas Gohr * @param mixed $default What to return if the value isn't set 3268da7d805SAndreas Gohr * @return mixed 3278da7d805SAndreas Gohr * @throws \PDOException 3288da7d805SAndreas Gohr */ 3298da7d805SAndreas Gohr public function getOpt($opt, $default = null) 3308da7d805SAndreas Gohr { 3318da7d805SAndreas Gohr $value = $this->queryValue("SELECT val FROM opts WHERE opt = ?", [$opt]); 332c9d29defSAndreas Gohr if ($value === null) { 333c9d29defSAndreas Gohr return $default; 334c9d29defSAndreas Gohr } 3358da7d805SAndreas Gohr return $value; 3368da7d805SAndreas Gohr } 3378da7d805SAndreas Gohr 3388da7d805SAndreas Gohr /** 3398da7d805SAndreas Gohr * Set a config value in the opt table 3408da7d805SAndreas Gohr * 3418da7d805SAndreas Gohr * @param $opt 3428da7d805SAndreas Gohr * @param $value 3438da7d805SAndreas Gohr * @throws \PDOException 3448da7d805SAndreas Gohr */ 3458da7d805SAndreas Gohr public function setOpt($opt, $value) 3468da7d805SAndreas Gohr { 3478da7d805SAndreas Gohr $this->exec('REPLACE INTO opts (opt,val) VALUES (?,?)', [$opt, $value]); 3488da7d805SAndreas Gohr } 3498da7d805SAndreas Gohr 3508da7d805SAndreas Gohr /** 3518da7d805SAndreas Gohr * @return string 3528da7d805SAndreas Gohr */ 3538da7d805SAndreas Gohr public function getDbName() 3548da7d805SAndreas Gohr { 3558da7d805SAndreas Gohr return $this->dbname; 3568da7d805SAndreas Gohr } 3578da7d805SAndreas Gohr 3588da7d805SAndreas Gohr /** 3598da7d805SAndreas Gohr * @return string 3608da7d805SAndreas Gohr */ 3618da7d805SAndreas Gohr public function getDbFile() 3628da7d805SAndreas Gohr { 3638da7d805SAndreas Gohr global $conf; 3648da7d805SAndreas Gohr return $conf['metadir'] . '/' . $this->dbname . self::FILE_EXTENSION; 3658da7d805SAndreas Gohr } 3668da7d805SAndreas Gohr 3678da7d805SAndreas Gohr /** 3688da7d805SAndreas Gohr * Create a dump of the database and its contents 3698da7d805SAndreas Gohr * 3708da7d805SAndreas Gohr * @return string 3718da7d805SAndreas Gohr * @throws \Exception 3728da7d805SAndreas Gohr */ 3738da7d805SAndreas Gohr public function dumpToFile($filename) 3748da7d805SAndreas Gohr { 3758da7d805SAndreas Gohr $fp = fopen($filename, 'w'); 3768da7d805SAndreas Gohr if (!$fp) { 3778da7d805SAndreas Gohr throw new \Exception('Could not open file ' . $filename . ' for writing'); 3788da7d805SAndreas Gohr } 3798da7d805SAndreas Gohr 3807ddaad11SAndreas Gohr $tables = $this->queryAll("SELECT name,sql FROM sqlite_master WHERE type='table'"); 3817ddaad11SAndreas Gohr $indexes = $this->queryAll("SELECT name,sql FROM sqlite_master WHERE type='index'"); 3828da7d805SAndreas Gohr 3838da7d805SAndreas Gohr foreach ($tables as $table) { 3847ddaad11SAndreas Gohr fwrite($fp, "DROP TABLE IF EXISTS '{$table['name']}';\n"); 3857ddaad11SAndreas Gohr } 3868da7d805SAndreas Gohr 3877ddaad11SAndreas Gohr foreach ($tables as $table) { 3887ddaad11SAndreas Gohr fwrite($fp, $table['sql'] . ";\n"); 3897ddaad11SAndreas Gohr } 3907ddaad11SAndreas Gohr 3917ddaad11SAndreas Gohr foreach ($tables as $table) { 3928da7d805SAndreas Gohr $sql = "SELECT * FROM " . $table['name']; 3938da7d805SAndreas Gohr $res = $this->query($sql); 3948da7d805SAndreas Gohr while ($row = $res->fetch(\PDO::FETCH_ASSOC)) { 395a7a40fb2SAnna Dabrowska $values = implode(',', array_map(function ($value) { 3967ddaad11SAndreas Gohr if ($value === null) return 'NULL'; 3977ddaad11SAndreas Gohr return $this->pdo->quote($value); 3987ddaad11SAndreas Gohr }, $row)); 3997ddaad11SAndreas Gohr fwrite($fp, "INSERT INTO '{$table['name']}' VALUES ({$values});\n"); 4008da7d805SAndreas Gohr } 4018da7d805SAndreas Gohr $res->closeCursor(); 4028da7d805SAndreas Gohr } 4038da7d805SAndreas Gohr 4048da7d805SAndreas Gohr foreach ($indexes as $index) { 4058da7d805SAndreas Gohr fwrite($fp, $index['sql'] . ";\n"); 4068da7d805SAndreas Gohr } 4078da7d805SAndreas Gohr fclose($fp); 4088da7d805SAndreas Gohr return $filename; 4098da7d805SAndreas Gohr } 4108da7d805SAndreas Gohr 4118da7d805SAndreas Gohr // endregion 4128da7d805SAndreas Gohr 4138da7d805SAndreas Gohr // region migration handling 4148da7d805SAndreas Gohr 4158da7d805SAndreas Gohr /** 4168da7d805SAndreas Gohr * Apply all pending migrations 4178da7d805SAndreas Gohr * 4188da7d805SAndreas Gohr * Each migration is executed in a transaction which is rolled back on failure 4198da7d805SAndreas Gohr * Migrations can be files in the schema directory or event handlers 4208da7d805SAndreas Gohr * 4218da7d805SAndreas Gohr * @throws \Exception 4228da7d805SAndreas Gohr */ 4238da7d805SAndreas Gohr protected function applyMigrations() 4248da7d805SAndreas Gohr { 4258da7d805SAndreas Gohr $currentVersion = $this->currentDbVersion(); 4268da7d805SAndreas Gohr $latestVersion = $this->latestDbVersion(); 4278da7d805SAndreas Gohr 428c70cffc9SAndreas Gohr if ($currentVersion === $latestVersion) return; 429c70cffc9SAndreas Gohr 4308da7d805SAndreas Gohr for ($newVersion = $currentVersion + 1; $newVersion <= $latestVersion; $newVersion++) { 4318da7d805SAndreas Gohr $data = [ 4328da7d805SAndreas Gohr 'dbname' => $this->dbname, 4338da7d805SAndreas Gohr 'from' => $currentVersion, 4348da7d805SAndreas Gohr 'to' => $newVersion, 4358da7d805SAndreas Gohr 'file' => $this->getMigrationFile($newVersion), 4368da7d805SAndreas Gohr 'sqlite' => $this->helper, 4378da7d805SAndreas Gohr 'adapter' => $this, 4388da7d805SAndreas Gohr ]; 439a7a40fb2SAnna Dabrowska $event = new Event('PLUGIN_SQLITE_DATABASE_UPGRADE', $data); 4408da7d805SAndreas Gohr 4418da7d805SAndreas Gohr $this->pdo->beginTransaction(); 4428da7d805SAndreas Gohr try { 4438da7d805SAndreas Gohr if ($event->advise_before()) { 4448da7d805SAndreas Gohr // standard migration file 4457ddaad11SAndreas Gohr $sql = Tools::SQLstring2array(file_get_contents($data['file'])); 4467ddaad11SAndreas Gohr foreach ($sql as $query) { 4477ddaad11SAndreas Gohr $this->pdo->exec($query); 4487ddaad11SAndreas Gohr } 449a7a40fb2SAnna Dabrowska } elseif (!$event->result) { 4508da7d805SAndreas Gohr // advise before returned false, but the result was false 4518da7d805SAndreas Gohr throw new \PDOException('Plugin event did not signal success'); 4528da7d805SAndreas Gohr } 4538da7d805SAndreas Gohr $this->setOpt('dbversion', $newVersion); 4548da7d805SAndreas Gohr $this->pdo->commit(); 4558da7d805SAndreas Gohr $event->advise_after(); 4568da7d805SAndreas Gohr } catch (\Exception $e) { 4578da7d805SAndreas Gohr // something went wrong, rollback 4588da7d805SAndreas Gohr $this->pdo->rollBack(); 4598da7d805SAndreas Gohr throw $e; 4608da7d805SAndreas Gohr } 4618da7d805SAndreas Gohr } 4628da7d805SAndreas Gohr 4638da7d805SAndreas Gohr // vacuum the database to free up unused space 4648da7d805SAndreas Gohr $this->pdo->exec('VACUUM'); 4658da7d805SAndreas Gohr } 4668da7d805SAndreas Gohr 4678da7d805SAndreas Gohr /** 4688da7d805SAndreas Gohr * Read the current version from the opt table 4698da7d805SAndreas Gohr * 4708da7d805SAndreas Gohr * The opt table is created here if not found 4718da7d805SAndreas Gohr * 4728da7d805SAndreas Gohr * @return int 4738da7d805SAndreas Gohr * @throws \PDOException 4748da7d805SAndreas Gohr */ 4758da7d805SAndreas Gohr protected function currentDbVersion() 4768da7d805SAndreas Gohr { 4778da7d805SAndreas Gohr try { 4788da7d805SAndreas Gohr $version = $this->getOpt('dbversion', 0); 4798da7d805SAndreas Gohr return (int)$version; 480e49844fbSAndreas Gohr } catch (\PDOException $e) { 481f71fd150SAndreas Gohr if (!preg_match('/no such table/', $e->getMessage())) { 482f71fd150SAndreas Gohr // if this is not a "no such table" error, there is something wrong see #80 483e49844fbSAndreas Gohr Logger::error( 484f71fd150SAndreas Gohr 'SQLite: Could not read dbversion from opt table due to unexpected error', 485e49844fbSAndreas Gohr [ 486e49844fbSAndreas Gohr 'dbname' => $this->dbname, 487e49844fbSAndreas Gohr 'exception' => get_class($e), 488e49844fbSAndreas Gohr 'message' => $e->getMessage(), 489e49844fbSAndreas Gohr 'code' => $e->getCode(), 490e49844fbSAndreas Gohr ], 491e49844fbSAndreas Gohr __FILE__, 492e49844fbSAndreas Gohr __LINE__ 493e49844fbSAndreas Gohr ); 494f71fd150SAndreas Gohr } 495e49844fbSAndreas Gohr 4968da7d805SAndreas Gohr // add the opt table - if this fails too, let the exception bubble up 4978da7d805SAndreas Gohr $sql = "CREATE TABLE IF NOT EXISTS opts (opt TEXT NOT NULL PRIMARY KEY, val NOT NULL DEFAULT '')"; 4988da7d805SAndreas Gohr $this->exec($sql); 4998da7d805SAndreas Gohr return 0; 5008da7d805SAndreas Gohr } 5018da7d805SAndreas Gohr } 5028da7d805SAndreas Gohr 5038da7d805SAndreas Gohr /** 5048da7d805SAndreas Gohr * Get the version this db should have 5058da7d805SAndreas Gohr * 5068da7d805SAndreas Gohr * @return int 5078da7d805SAndreas Gohr * @throws \PDOException 5088da7d805SAndreas Gohr */ 5098da7d805SAndreas Gohr protected function latestDbVersion() 5108da7d805SAndreas Gohr { 5118da7d805SAndreas Gohr if (!file_exists($this->schemadir . '/latest.version')) { 5128da7d805SAndreas Gohr throw new \PDOException('No latest.version in schema dir'); 5138da7d805SAndreas Gohr } 5148da7d805SAndreas Gohr return (int)trim(file_get_contents($this->schemadir . '/latest.version')); 5158da7d805SAndreas Gohr } 5168da7d805SAndreas Gohr 5178da7d805SAndreas Gohr /** 5188da7d805SAndreas Gohr * Get the migrartion file for the given version 5198da7d805SAndreas Gohr * 5208da7d805SAndreas Gohr * @param int $version 5218da7d805SAndreas Gohr * @return string 5228da7d805SAndreas Gohr */ 5238da7d805SAndreas Gohr protected function getMigrationFile($version) 5248da7d805SAndreas Gohr { 5258da7d805SAndreas Gohr return sprintf($this->schemadir . '/update%04d.sql', $version); 5268da7d805SAndreas Gohr } 5278da7d805SAndreas Gohr // endregion 5288da7d805SAndreas Gohr} 529