xref: /plugin/sqlite/SQLiteDB.php (revision e062c24746ec4c9c598cea7cb12f0486c953293e)
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    {
4448c6692fSAnna Dabrowska        global $plugin_controller;
4548c6692fSAnna Dabrowska        if (!$plugin_controller->isEnabled('sqlite')) {
4648c6692fSAnna Dabrowska            throw new \Exception('SQLite plugin seems to be disabled.');
4748c6692fSAnna 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    {
248*e062c247Ssplitbrain        $columns = array_map(static fn($column) => '"' . $column . '"', array_keys($data));
2498da7d805SAndreas Gohr        $values = array_values($data);
2508da7d805SAndreas Gohr        $placeholders = array_pad([], count($columns), '?');
2518da7d805SAndreas Gohr
2528da7d805SAndreas Gohr        if ($replace) {
2538da7d805SAndreas Gohr            $command = 'REPLACE';
2548da7d805SAndreas Gohr        } else {
2558da7d805SAndreas Gohr            $command = 'INSERT OR IGNORE';
2568da7d805SAndreas Gohr        }
2578da7d805SAndreas Gohr
2588da7d805SAndreas Gohr        /** @noinspection SqlResolve */
259a7a40fb2SAnna Dabrowska        $sql = $command . ' INTO "' . $table . '" (' . implode(',', $columns) . ') VALUES (' . implode(
260a7a40fb2SAnna Dabrowska            ',',
261a7a40fb2SAnna Dabrowska            $placeholders
262a7a40fb2SAnna Dabrowska        ) . ')';
263b35b734aSSzymon Olewniczak        $stm = $this->query($sql, $values);
264a7a36cdbSAndreas Gohr        $success = $stm->rowCount();
2658da7d805SAndreas Gohr        $stm->closeCursor();
266a7a36cdbSAndreas Gohr
267a7a36cdbSAndreas Gohr        if ($success) {
268a7a36cdbSAndreas Gohr            $sql = 'SELECT * FROM "' . $table . '" WHERE rowid = last_insert_rowid()';
269a7a36cdbSAndreas Gohr            return $this->queryRecord($sql);
270a7a36cdbSAndreas Gohr        }
271a7a36cdbSAndreas Gohr        return null;
2728da7d805SAndreas Gohr    }
2738da7d805SAndreas Gohr
2748da7d805SAndreas Gohr    /**
2758da7d805SAndreas Gohr     * Execute a query that returns a single value
2768da7d805SAndreas Gohr     *
2778da7d805SAndreas Gohr     * @param string $sql
27827eb38daSAndreas Gohr     * @param ...mixed|array $params
2798da7d805SAndreas Gohr     * @return mixed|null
2808da7d805SAndreas Gohr     * @throws \PDOException
2818da7d805SAndreas Gohr     */
28227eb38daSAndreas Gohr    public function queryValue($sql, ...$params)
2838da7d805SAndreas Gohr    {
28427eb38daSAndreas Gohr        $result = $this->queryAll($sql, ...$params);
285c9d29defSAndreas Gohr        if (is_array($result) && count($result)) {
286c9d29defSAndreas Gohr            return array_values($result[0])[0];
287c9d29defSAndreas Gohr        }
2888da7d805SAndreas Gohr        return null;
2898da7d805SAndreas Gohr    }
2908da7d805SAndreas Gohr
29133e488b3SAndreas Gohr    /**
29233e488b3SAndreas Gohr     * Execute a query that returns a list of key-value pairs
29333e488b3SAndreas Gohr     *
29433e488b3SAndreas Gohr     * The first column is used as key, the second as value. Any additional colums are ignored.
29533e488b3SAndreas Gohr     *
29633e488b3SAndreas Gohr     * @param string $sql
29727eb38daSAndreas Gohr     * @param ...mixed|array $params
29833e488b3SAndreas Gohr     * @return array
29933e488b3SAndreas Gohr     */
30027eb38daSAndreas Gohr    public function queryKeyValueList($sql, ...$params)
30133e488b3SAndreas Gohr    {
30227eb38daSAndreas Gohr        $result = $this->queryAll($sql, ...$params);
30333e488b3SAndreas Gohr        if (!$result) return [];
30433e488b3SAndreas Gohr        if (count(array_keys($result[0])) != 2) {
30533e488b3SAndreas Gohr            throw new \RuntimeException('queryKeyValueList expects a query that returns exactly two columns');
30633e488b3SAndreas Gohr        }
30733e488b3SAndreas Gohr        [$key, $val] = array_keys($result[0]);
30833e488b3SAndreas Gohr
30933e488b3SAndreas Gohr        return array_combine(
31033e488b3SAndreas Gohr            array_column($result, $key),
31133e488b3SAndreas Gohr            array_column($result, $val)
31233e488b3SAndreas Gohr        );
31333e488b3SAndreas Gohr    }
31433e488b3SAndreas Gohr
3158da7d805SAndreas Gohr    // endregion
3168da7d805SAndreas Gohr
3178da7d805SAndreas Gohr    // region meta handling
3188da7d805SAndreas Gohr
3198da7d805SAndreas Gohr    /**
3208da7d805SAndreas Gohr     * Get a config value from the opt table
3218da7d805SAndreas Gohr     *
3228da7d805SAndreas Gohr     * @param string $opt Config name
3238da7d805SAndreas Gohr     * @param mixed $default What to return if the value isn't set
3248da7d805SAndreas Gohr     * @return mixed
3258da7d805SAndreas Gohr     * @throws \PDOException
3268da7d805SAndreas Gohr     */
3278da7d805SAndreas Gohr    public function getOpt($opt, $default = null)
3288da7d805SAndreas Gohr    {
3298da7d805SAndreas Gohr        $value = $this->queryValue("SELECT val FROM opts WHERE opt = ?", [$opt]);
330c9d29defSAndreas Gohr        if ($value === null) {
331c9d29defSAndreas Gohr            return $default;
332c9d29defSAndreas Gohr        }
3338da7d805SAndreas Gohr        return $value;
3348da7d805SAndreas Gohr    }
3358da7d805SAndreas Gohr
3368da7d805SAndreas Gohr    /**
3378da7d805SAndreas Gohr     * Set a config value in the opt table
3388da7d805SAndreas Gohr     *
3398da7d805SAndreas Gohr     * @param $opt
3408da7d805SAndreas Gohr     * @param $value
3418da7d805SAndreas Gohr     * @throws \PDOException
3428da7d805SAndreas Gohr     */
3438da7d805SAndreas Gohr    public function setOpt($opt, $value)
3448da7d805SAndreas Gohr    {
3458da7d805SAndreas Gohr        $this->exec('REPLACE INTO opts (opt,val) VALUES (?,?)', [$opt, $value]);
3468da7d805SAndreas Gohr    }
3478da7d805SAndreas Gohr
3488da7d805SAndreas Gohr    /**
3498da7d805SAndreas Gohr     * @return string
3508da7d805SAndreas Gohr     */
3518da7d805SAndreas Gohr    public function getDbName()
3528da7d805SAndreas Gohr    {
3538da7d805SAndreas Gohr        return $this->dbname;
3548da7d805SAndreas Gohr    }
3558da7d805SAndreas Gohr
3568da7d805SAndreas Gohr    /**
3578da7d805SAndreas Gohr     * @return string
3588da7d805SAndreas Gohr     */
3598da7d805SAndreas Gohr    public function getDbFile()
3608da7d805SAndreas Gohr    {
3618da7d805SAndreas Gohr        global $conf;
3628da7d805SAndreas Gohr        return $conf['metadir'] . '/' . $this->dbname . self::FILE_EXTENSION;
3638da7d805SAndreas Gohr    }
3648da7d805SAndreas Gohr
3658da7d805SAndreas Gohr    /**
3668da7d805SAndreas Gohr     * Create a dump of the database and its contents
3678da7d805SAndreas Gohr     *
3688da7d805SAndreas Gohr     * @return string
3698da7d805SAndreas Gohr     * @throws \Exception
3708da7d805SAndreas Gohr     */
3718da7d805SAndreas Gohr    public function dumpToFile($filename)
3728da7d805SAndreas Gohr    {
3738da7d805SAndreas Gohr        $fp = fopen($filename, 'w');
3748da7d805SAndreas Gohr        if (!$fp) {
3758da7d805SAndreas Gohr            throw new \Exception('Could not open file ' . $filename . ' for writing');
3768da7d805SAndreas Gohr        }
3778da7d805SAndreas Gohr
3787ddaad11SAndreas Gohr        $tables = $this->queryAll("SELECT name,sql FROM sqlite_master WHERE type='table'");
3797ddaad11SAndreas Gohr        $indexes = $this->queryAll("SELECT name,sql FROM sqlite_master WHERE type='index'");
3808da7d805SAndreas Gohr
3818da7d805SAndreas Gohr        foreach ($tables as $table) {
3827ddaad11SAndreas Gohr            fwrite($fp, "DROP TABLE IF EXISTS '{$table['name']}';\n");
3837ddaad11SAndreas Gohr        }
3848da7d805SAndreas Gohr
3857ddaad11SAndreas Gohr        foreach ($tables as $table) {
3867ddaad11SAndreas Gohr            fwrite($fp, $table['sql'] . ";\n");
3877ddaad11SAndreas Gohr        }
3887ddaad11SAndreas Gohr
3897ddaad11SAndreas Gohr        foreach ($tables as $table) {
3908da7d805SAndreas Gohr            $sql = "SELECT * FROM " . $table['name'];
3918da7d805SAndreas Gohr            $res = $this->query($sql);
3928da7d805SAndreas Gohr            while ($row = $res->fetch(\PDO::FETCH_ASSOC)) {
393a7a40fb2SAnna Dabrowska                $values = implode(',', array_map(function ($value) {
3947ddaad11SAndreas Gohr                    if ($value === null) return 'NULL';
3957ddaad11SAndreas Gohr                    return $this->pdo->quote($value);
3967ddaad11SAndreas Gohr                }, $row));
3977ddaad11SAndreas Gohr                fwrite($fp, "INSERT INTO '{$table['name']}' VALUES ({$values});\n");
3988da7d805SAndreas Gohr            }
3998da7d805SAndreas Gohr            $res->closeCursor();
4008da7d805SAndreas Gohr        }
4018da7d805SAndreas Gohr
4028da7d805SAndreas Gohr        foreach ($indexes as $index) {
4038da7d805SAndreas Gohr            fwrite($fp, $index['sql'] . ";\n");
4048da7d805SAndreas Gohr        }
4058da7d805SAndreas Gohr        fclose($fp);
4068da7d805SAndreas Gohr        return $filename;
4078da7d805SAndreas Gohr    }
4088da7d805SAndreas Gohr
4098da7d805SAndreas Gohr    // endregion
4108da7d805SAndreas Gohr
4118da7d805SAndreas Gohr    // region migration handling
4128da7d805SAndreas Gohr
4138da7d805SAndreas Gohr    /**
4148da7d805SAndreas Gohr     * Apply all pending migrations
4158da7d805SAndreas Gohr     *
4168da7d805SAndreas Gohr     * Each migration is executed in a transaction which is rolled back on failure
4178da7d805SAndreas Gohr     * Migrations can be files in the schema directory or event handlers
4188da7d805SAndreas Gohr     *
4198da7d805SAndreas Gohr     * @throws \Exception
4208da7d805SAndreas Gohr     */
4218da7d805SAndreas Gohr    protected function applyMigrations()
4228da7d805SAndreas Gohr    {
4238da7d805SAndreas Gohr        $currentVersion = $this->currentDbVersion();
4248da7d805SAndreas Gohr        $latestVersion = $this->latestDbVersion();
4258da7d805SAndreas Gohr
426c70cffc9SAndreas Gohr        if ($currentVersion === $latestVersion) return;
427c70cffc9SAndreas Gohr
4288da7d805SAndreas Gohr        for ($newVersion = $currentVersion + 1; $newVersion <= $latestVersion; $newVersion++) {
4298da7d805SAndreas Gohr            $data = [
4308da7d805SAndreas Gohr                'dbname' => $this->dbname,
4318da7d805SAndreas Gohr                'from' => $currentVersion,
4328da7d805SAndreas Gohr                'to' => $newVersion,
4338da7d805SAndreas Gohr                'file' => $this->getMigrationFile($newVersion),
4348da7d805SAndreas Gohr                'sqlite' => $this->helper,
4358da7d805SAndreas Gohr                'adapter' => $this,
4368da7d805SAndreas Gohr            ];
437a7a40fb2SAnna Dabrowska            $event = new Event('PLUGIN_SQLITE_DATABASE_UPGRADE', $data);
4388da7d805SAndreas Gohr
4398da7d805SAndreas Gohr            $this->pdo->beginTransaction();
4408da7d805SAndreas Gohr            try {
4418da7d805SAndreas Gohr                if ($event->advise_before()) {
4428da7d805SAndreas Gohr                    // standard migration file
4437ddaad11SAndreas Gohr                    $sql = Tools::SQLstring2array(file_get_contents($data['file']));
4447ddaad11SAndreas Gohr                    foreach ($sql as $query) {
4457ddaad11SAndreas Gohr                        $this->pdo->exec($query);
4467ddaad11SAndreas Gohr                    }
447a7a40fb2SAnna Dabrowska                } elseif (!$event->result) {
4488da7d805SAndreas Gohr                    // advise before returned false, but the result was false
4498da7d805SAndreas Gohr                    throw new \PDOException('Plugin event did not signal success');
4508da7d805SAndreas Gohr                }
4518da7d805SAndreas Gohr                $this->setOpt('dbversion', $newVersion);
4528da7d805SAndreas Gohr                $this->pdo->commit();
4538da7d805SAndreas Gohr                $event->advise_after();
4548da7d805SAndreas Gohr            } catch (\Exception $e) {
4558da7d805SAndreas Gohr                // something went wrong, rollback
4568da7d805SAndreas Gohr                $this->pdo->rollBack();
4578da7d805SAndreas Gohr                throw $e;
4588da7d805SAndreas Gohr            }
4598da7d805SAndreas Gohr        }
4608da7d805SAndreas Gohr
4618da7d805SAndreas Gohr        // vacuum the database to free up unused space
4628da7d805SAndreas Gohr        $this->pdo->exec('VACUUM');
4638da7d805SAndreas Gohr    }
4648da7d805SAndreas Gohr
4658da7d805SAndreas Gohr    /**
4668da7d805SAndreas Gohr     * Read the current version from the opt table
4678da7d805SAndreas Gohr     *
4688da7d805SAndreas Gohr     * The opt table is created here if not found
4698da7d805SAndreas Gohr     *
4708da7d805SAndreas Gohr     * @return int
4718da7d805SAndreas Gohr     * @throws \PDOException
4728da7d805SAndreas Gohr     */
4738da7d805SAndreas Gohr    protected function currentDbVersion()
4748da7d805SAndreas Gohr    {
4758da7d805SAndreas Gohr        try {
4768da7d805SAndreas Gohr            $version = $this->getOpt('dbversion', 0);
4778da7d805SAndreas Gohr            return (int)$version;
478e49844fbSAndreas Gohr        } catch (\PDOException $e) {
479f71fd150SAndreas Gohr            if (!preg_match('/no such table/', $e->getMessage())) {
480f71fd150SAndreas Gohr                // if this is not a "no such table" error, there is something wrong see #80
481e49844fbSAndreas Gohr                Logger::error(
482f71fd150SAndreas Gohr                    'SQLite: Could not read dbversion from opt table due to unexpected error',
483e49844fbSAndreas Gohr                    [
484e49844fbSAndreas Gohr                        'dbname' => $this->dbname,
485e49844fbSAndreas Gohr                        'exception' => get_class($e),
486e49844fbSAndreas Gohr                        'message' => $e->getMessage(),
487e49844fbSAndreas Gohr                        'code' => $e->getCode(),
488e49844fbSAndreas Gohr                    ],
489e49844fbSAndreas Gohr                    __FILE__,
490e49844fbSAndreas Gohr                    __LINE__
491e49844fbSAndreas Gohr                );
492f71fd150SAndreas Gohr            }
493e49844fbSAndreas Gohr
4948da7d805SAndreas Gohr            // add the opt table - if this fails too, let the exception bubble up
4958da7d805SAndreas Gohr            $sql = "CREATE TABLE IF NOT EXISTS opts (opt TEXT NOT NULL PRIMARY KEY, val NOT NULL DEFAULT '')";
4968da7d805SAndreas Gohr            $this->exec($sql);
4978da7d805SAndreas Gohr            return 0;
4988da7d805SAndreas Gohr        }
4998da7d805SAndreas Gohr    }
5008da7d805SAndreas Gohr
5018da7d805SAndreas Gohr    /**
5028da7d805SAndreas Gohr     * Get the version this db should have
5038da7d805SAndreas Gohr     *
5048da7d805SAndreas Gohr     * @return int
5058da7d805SAndreas Gohr     * @throws \PDOException
5068da7d805SAndreas Gohr     */
5078da7d805SAndreas Gohr    protected function latestDbVersion()
5088da7d805SAndreas Gohr    {
5098da7d805SAndreas Gohr        if (!file_exists($this->schemadir . '/latest.version')) {
5108da7d805SAndreas Gohr            throw new \PDOException('No latest.version in schema dir');
5118da7d805SAndreas Gohr        }
5128da7d805SAndreas Gohr        return (int)trim(file_get_contents($this->schemadir . '/latest.version'));
5138da7d805SAndreas Gohr    }
5148da7d805SAndreas Gohr
5158da7d805SAndreas Gohr    /**
5168da7d805SAndreas Gohr     * Get the migrartion file for the given version
5178da7d805SAndreas Gohr     *
5188da7d805SAndreas Gohr     * @param int $version
5198da7d805SAndreas Gohr     * @return string
5208da7d805SAndreas Gohr     */
5218da7d805SAndreas Gohr    protected function getMigrationFile($version)
5228da7d805SAndreas Gohr    {
5238da7d805SAndreas Gohr        return sprintf($this->schemadir . '/update%04d.sql', $version);
5248da7d805SAndreas Gohr    }
5258da7d805SAndreas Gohr    // endregion
5268da7d805SAndreas Gohr}
527