xref: /plugin/sqlite/SQLiteDB.php (revision e49844fb9312b6135b8528c7da0d1b780d9f6573)
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
11*e49844fbSAndreas Gohruse dokuwiki\ErrorHandler;
12b35b734aSSzymon Olewniczakuse dokuwiki\Extension\Event;
130290deaeSAndreas Gohruse dokuwiki\Logger;
148da7d805SAndreas Gohr
158da7d805SAndreas Gohr/**
168da7d805SAndreas Gohr * Helpers to access a SQLite Database with automatic schema migration
178da7d805SAndreas Gohr */
188da7d805SAndreas Gohrclass SQLiteDB
198da7d805SAndreas Gohr{
208da7d805SAndreas Gohr    const FILE_EXTENSION = '.sqlite3';
218da7d805SAndreas Gohr
228da7d805SAndreas Gohr    /** @var \PDO */
238da7d805SAndreas Gohr    protected $pdo;
248da7d805SAndreas Gohr
258da7d805SAndreas Gohr    /** @var string */
268da7d805SAndreas Gohr    protected $schemadir;
278da7d805SAndreas Gohr
288da7d805SAndreas Gohr    /** @var string */
298da7d805SAndreas Gohr    protected $dbname;
308da7d805SAndreas Gohr
313a56750bSAndreas Gohr    /** @var \helper_plugin_sqlite */
328da7d805SAndreas Gohr    protected $helper;
338da7d805SAndreas Gohr
34fe64ba38SAndreas Gohr
358da7d805SAndreas Gohr    /**
368da7d805SAndreas Gohr     * Constructor
378da7d805SAndreas Gohr     *
388da7d805SAndreas Gohr     * @param string $dbname Database name
398da7d805SAndreas Gohr     * @param string $schemadir directory with schema migration files
408da7d805SAndreas Gohr     * @param \helper_plugin_sqlite $sqlitehelper for backwards compatibility
418da7d805SAndreas Gohr     * @throws \Exception
428da7d805SAndreas Gohr     */
438da7d805SAndreas Gohr    public function __construct($dbname, $schemadir, $sqlitehelper = null)
448da7d805SAndreas Gohr    {
458da7d805SAndreas Gohr        if (!class_exists('pdo') || !in_array('sqlite', \PDO::getAvailableDrivers())) {
468da7d805SAndreas Gohr            throw new \Exception('SQLite PDO driver not available');
478da7d805SAndreas Gohr        }
488da7d805SAndreas Gohr
498da7d805SAndreas Gohr        // backwards compatibility, circular dependency
508da7d805SAndreas Gohr        $this->helper = $sqlitehelper;
513a56750bSAndreas Gohr        if (!$this->helper) {
523a56750bSAndreas Gohr            $this->helper = new \helper_plugin_sqlite();
533a56750bSAndreas Gohr        }
543a56750bSAndreas Gohr        $this->helper->setAdapter($this);
558da7d805SAndreas Gohr
568da7d805SAndreas Gohr        $this->schemadir = $schemadir;
578da7d805SAndreas Gohr        $this->dbname = $dbname;
588da7d805SAndreas Gohr        $file = $this->getDbFile();
598da7d805SAndreas Gohr
608da7d805SAndreas Gohr        $this->pdo = new \PDO(
618da7d805SAndreas Gohr            'sqlite:' . $file,
628da7d805SAndreas Gohr            null,
638da7d805SAndreas Gohr            null,
648da7d805SAndreas Gohr            [
658da7d805SAndreas Gohr                \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
668da7d805SAndreas Gohr            ]
678da7d805SAndreas Gohr        );
688da7d805SAndreas Gohr
698da7d805SAndreas Gohr        if ($schemadir !== '') {
708da7d805SAndreas Gohr            // schema dir is empty, when accessing the DB from Admin interface instead of plugin context
718da7d805SAndreas Gohr            $this->applyMigrations();
728da7d805SAndreas Gohr        }
738da7d805SAndreas Gohr        Functions::register($this->pdo);
748da7d805SAndreas Gohr    }
758da7d805SAndreas Gohr
76aae177f9SAndreas Gohr    /**
77aae177f9SAndreas Gohr     * Do not serialize the DB connection
78aae177f9SAndreas Gohr     *
79aae177f9SAndreas Gohr     * @return array
80aae177f9SAndreas Gohr     */
81c9d29defSAndreas Gohr    public function __sleep()
82c9d29defSAndreas Gohr    {
83aae177f9SAndreas Gohr        $this->pdo = null;
84aae177f9SAndreas Gohr        return array_keys(get_object_vars($this));
85aae177f9SAndreas Gohr    }
86aae177f9SAndreas Gohr
87aae177f9SAndreas Gohr    /**
88aae177f9SAndreas Gohr     * On deserialization, reinit database connection
89aae177f9SAndreas Gohr     */
90c9d29defSAndreas Gohr    public function __wakeup()
91c9d29defSAndreas Gohr    {
92aae177f9SAndreas Gohr        $this->__construct($this->dbname, $this->schemadir, $this->helper);
93aae177f9SAndreas Gohr    }
948da7d805SAndreas Gohr
958da7d805SAndreas Gohr    // region public API
968da7d805SAndreas Gohr
978da7d805SAndreas Gohr    /**
988da7d805SAndreas Gohr     * Direct access to the PDO object
998da7d805SAndreas Gohr     * @return \PDO
1008da7d805SAndreas Gohr     */
10133e488b3SAndreas Gohr    public function getPdo()
10233e488b3SAndreas Gohr    {
103e22957e9SSzymon Olewniczak        return $this->pdo;
104801b921eSSzymon Olewniczak    }
105801b921eSSzymon Olewniczak
106801b921eSSzymon Olewniczak    /**
1078da7d805SAndreas Gohr     * Execute a statement and return it
1088da7d805SAndreas Gohr     *
1098da7d805SAndreas Gohr     * @param string $sql
11027eb38daSAndreas Gohr     * @param ...mixed|array $parameters
1118da7d805SAndreas Gohr     * @return \PDOStatement Be sure to close the cursor yourself
1128da7d805SAndreas Gohr     * @throws \PDOException
1138da7d805SAndreas Gohr     */
11427eb38daSAndreas Gohr    public function query($sql, ...$parameters)
1158da7d805SAndreas Gohr    {
1160290deaeSAndreas Gohr        $start = microtime(true);
1170290deaeSAndreas Gohr
11827eb38daSAndreas Gohr        if ($parameters && is_array($parameters[0])) $parameters = $parameters[0];
11927eb38daSAndreas Gohr
12003f14a77SAndreas Gohr        // Statement preparation sometime throws ValueErrors instead of PDOExceptions, we streamline here
12103f14a77SAndreas Gohr        try {
1228da7d805SAndreas Gohr            $stmt = $this->pdo->prepare($sql);
12303f14a77SAndreas Gohr        } catch (\Throwable $e) {
12403f14a77SAndreas Gohr            throw new \PDOException($e->getMessage(), (int)$e->getCode(), $e);
12503f14a77SAndreas Gohr        }
126b35b734aSSzymon Olewniczak        $eventData = [
127b8ae4891SSzymon Olewniczak            'sqlitedb' => $this,
128b35b734aSSzymon Olewniczak            'sql' => &$sql,
129b35b734aSSzymon Olewniczak            'parameters' => &$parameters,
130b35b734aSSzymon Olewniczak            'stmt' => $stmt
131b35b734aSSzymon Olewniczak        ];
132b35b734aSSzymon Olewniczak        $event = new Event('PLUGIN_SQLITE_QUERY_EXECUTE', $eventData);
133b35b734aSSzymon Olewniczak        if ($event->advise_before()) {
1348da7d805SAndreas Gohr            $stmt->execute($parameters);
135b35b734aSSzymon Olewniczak        }
136b35b734aSSzymon Olewniczak        $event->advise_after();
1370290deaeSAndreas Gohr
1380290deaeSAndreas Gohr        $time = microtime(true) - $start;
1390290deaeSAndreas Gohr        if ($time > 0.2) {
1400290deaeSAndreas Gohr            Logger::debug('[sqlite] slow query:  (' . $time . 's)', [
1410290deaeSAndreas Gohr                'sql' => $sql,
1420290deaeSAndreas Gohr                'parameters' => $parameters,
1430290deaeSAndreas Gohr                'backtrace' => explode("\n", dbg_backtrace())
1440290deaeSAndreas Gohr            ]);
1450290deaeSAndreas Gohr        }
1460290deaeSAndreas Gohr
1478da7d805SAndreas Gohr        return $stmt;
1488da7d805SAndreas Gohr    }
1498da7d805SAndreas Gohr
1508da7d805SAndreas Gohr    /**
1518da7d805SAndreas Gohr     * Execute a statement and return metadata
1528da7d805SAndreas Gohr     *
1538da7d805SAndreas Gohr     * Returns the last insert ID on INSERTs or the number of affected rows
1548da7d805SAndreas Gohr     *
1558da7d805SAndreas Gohr     * @param string $sql
15627eb38daSAndreas Gohr     * @param ...mixed|array $parameters
1578da7d805SAndreas Gohr     * @return int
1588da7d805SAndreas Gohr     * @throws \PDOException
1598da7d805SAndreas Gohr     */
16027eb38daSAndreas Gohr    public function exec($sql, ...$parameters)
1618da7d805SAndreas Gohr    {
16227eb38daSAndreas Gohr        $stmt = $this->query($sql, ...$parameters);
1638da7d805SAndreas Gohr
1648da7d805SAndreas Gohr        $count = $stmt->rowCount();
1658da7d805SAndreas Gohr        $stmt->closeCursor();
1668da7d805SAndreas Gohr        if ($count && preg_match('/^INSERT /i', $sql)) {
1678da7d805SAndreas Gohr            return $this->queryValue('SELECT last_insert_rowid()');
1688da7d805SAndreas Gohr        }
1698da7d805SAndreas Gohr
1708da7d805SAndreas Gohr        return $count;
1718da7d805SAndreas Gohr    }
1728da7d805SAndreas Gohr
1738da7d805SAndreas Gohr    /**
1748da7d805SAndreas Gohr     * Simple query abstraction
1758da7d805SAndreas Gohr     *
1768da7d805SAndreas Gohr     * Returns all data
1778da7d805SAndreas Gohr     *
1788da7d805SAndreas Gohr     * @param string $sql
17927eb38daSAndreas Gohr     * @param ...mixed|array $params
1808da7d805SAndreas Gohr     * @return array
1818da7d805SAndreas Gohr     * @throws \PDOException
1828da7d805SAndreas Gohr     */
18327eb38daSAndreas Gohr    public function queryAll($sql, ...$params)
1848da7d805SAndreas Gohr    {
18527eb38daSAndreas Gohr        $stmt = $this->query($sql, ...$params);
1868da7d805SAndreas Gohr        $data = $stmt->fetchAll(\PDO::FETCH_ASSOC);
1878da7d805SAndreas Gohr        $stmt->closeCursor();
1888da7d805SAndreas Gohr        return $data;
1898da7d805SAndreas Gohr    }
1908da7d805SAndreas Gohr
1918da7d805SAndreas Gohr    /**
1928da7d805SAndreas Gohr     * Query one single row
1938da7d805SAndreas Gohr     *
1948da7d805SAndreas Gohr     * @param string $sql
19527eb38daSAndreas Gohr     * @param ...mixed|array $params
1968da7d805SAndreas Gohr     * @return array|null
1978da7d805SAndreas Gohr     * @throws \PDOException
1988da7d805SAndreas Gohr     */
19927eb38daSAndreas Gohr    public function queryRecord($sql, ...$params)
2008da7d805SAndreas Gohr    {
20127eb38daSAndreas Gohr        $stmt = $this->query($sql, ...$params);
202a7a36cdbSAndreas Gohr        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
2038da7d805SAndreas Gohr        $stmt->closeCursor();
204a7a36cdbSAndreas Gohr        if (is_array($row) && count($row)) {
205a7a36cdbSAndreas Gohr            return $row;
206a7a36cdbSAndreas Gohr        }
2078da7d805SAndreas Gohr        return null;
2088da7d805SAndreas Gohr    }
2098da7d805SAndreas Gohr
2108da7d805SAndreas Gohr    /**
2118da7d805SAndreas Gohr     * Insert or replace the given data into the table
2128da7d805SAndreas Gohr     *
2138da7d805SAndreas Gohr     * @param string $table
2148da7d805SAndreas Gohr     * @param array $data
2158da7d805SAndreas Gohr     * @param bool $replace Conflict resolution, replace or ignore
216a7a36cdbSAndreas Gohr     * @return array|null Either the inserted row or null if nothing was inserted
2178da7d805SAndreas Gohr     * @throws \PDOException
2188da7d805SAndreas Gohr     */
2198da7d805SAndreas Gohr    public function saveRecord($table, $data, $replace = true)
2208da7d805SAndreas Gohr    {
2218da7d805SAndreas Gohr        $columns = array_map(function ($column) {
2228da7d805SAndreas Gohr            return '"' . $column . '"';
2238da7d805SAndreas Gohr        }, array_keys($data));
2248da7d805SAndreas Gohr        $values = array_values($data);
2258da7d805SAndreas Gohr        $placeholders = array_pad([], count($columns), '?');
2268da7d805SAndreas Gohr
2278da7d805SAndreas Gohr        if ($replace) {
2288da7d805SAndreas Gohr            $command = 'REPLACE';
2298da7d805SAndreas Gohr        } else {
2308da7d805SAndreas Gohr            $command = 'INSERT OR IGNORE';
2318da7d805SAndreas Gohr        }
2328da7d805SAndreas Gohr
2338da7d805SAndreas Gohr        /** @noinspection SqlResolve */
234a7a36cdbSAndreas Gohr        $sql = $command . ' INTO "' . $table . '" (' . join(',', $columns) . ') VALUES (' . join(',',
235a7a36cdbSAndreas Gohr                $placeholders) . ')';
236b35b734aSSzymon Olewniczak        $stm = $this->query($sql, $values);
237a7a36cdbSAndreas Gohr        $success = $stm->rowCount();
2388da7d805SAndreas Gohr        $stm->closeCursor();
239a7a36cdbSAndreas Gohr
240a7a36cdbSAndreas Gohr        if ($success) {
241a7a36cdbSAndreas Gohr            $sql = 'SELECT * FROM "' . $table . '" WHERE rowid = last_insert_rowid()';
242a7a36cdbSAndreas Gohr            return $this->queryRecord($sql);
243a7a36cdbSAndreas Gohr        }
244a7a36cdbSAndreas Gohr        return null;
2458da7d805SAndreas Gohr    }
2468da7d805SAndreas Gohr
2478da7d805SAndreas Gohr    /**
2488da7d805SAndreas Gohr     * Execute a query that returns a single value
2498da7d805SAndreas Gohr     *
2508da7d805SAndreas Gohr     * @param string $sql
25127eb38daSAndreas Gohr     * @param ...mixed|array $params
2528da7d805SAndreas Gohr     * @return mixed|null
2538da7d805SAndreas Gohr     * @throws \PDOException
2548da7d805SAndreas Gohr     */
25527eb38daSAndreas Gohr    public function queryValue($sql, ...$params)
2568da7d805SAndreas Gohr    {
25727eb38daSAndreas Gohr        $result = $this->queryAll($sql, ...$params);
258c9d29defSAndreas Gohr        if (is_array($result) && count($result)) {
259c9d29defSAndreas Gohr            return array_values($result[0])[0];
260c9d29defSAndreas Gohr        }
2618da7d805SAndreas Gohr        return null;
2628da7d805SAndreas Gohr    }
2638da7d805SAndreas Gohr
26433e488b3SAndreas Gohr    /**
26533e488b3SAndreas Gohr     * Execute a query that returns a list of key-value pairs
26633e488b3SAndreas Gohr     *
26733e488b3SAndreas Gohr     * The first column is used as key, the second as value. Any additional colums are ignored.
26833e488b3SAndreas Gohr     *
26933e488b3SAndreas Gohr     * @param string $sql
27027eb38daSAndreas Gohr     * @param ...mixed|array $params
27133e488b3SAndreas Gohr     * @return array
27233e488b3SAndreas Gohr     */
27327eb38daSAndreas Gohr    public function queryKeyValueList($sql, ...$params)
27433e488b3SAndreas Gohr    {
27527eb38daSAndreas Gohr        $result = $this->queryAll($sql, ...$params);
27633e488b3SAndreas Gohr        if (!$result) return [];
27733e488b3SAndreas Gohr        if (count(array_keys($result[0])) != 2) {
27833e488b3SAndreas Gohr            throw new \RuntimeException('queryKeyValueList expects a query that returns exactly two columns');
27933e488b3SAndreas Gohr        }
28033e488b3SAndreas Gohr        [$key, $val] = array_keys($result[0]);
28133e488b3SAndreas Gohr
28233e488b3SAndreas Gohr        return array_combine(
28333e488b3SAndreas Gohr            array_column($result, $key),
28433e488b3SAndreas Gohr            array_column($result, $val)
28533e488b3SAndreas Gohr        );
28633e488b3SAndreas Gohr    }
28733e488b3SAndreas Gohr
2888da7d805SAndreas Gohr    // endregion
2898da7d805SAndreas Gohr
2908da7d805SAndreas Gohr    // region meta handling
2918da7d805SAndreas Gohr
2928da7d805SAndreas Gohr    /**
2938da7d805SAndreas Gohr     * Get a config value from the opt table
2948da7d805SAndreas Gohr     *
2958da7d805SAndreas Gohr     * @param string $opt Config name
2968da7d805SAndreas Gohr     * @param mixed $default What to return if the value isn't set
2978da7d805SAndreas Gohr     * @return mixed
2988da7d805SAndreas Gohr     * @throws \PDOException
2998da7d805SAndreas Gohr     */
3008da7d805SAndreas Gohr    public function getOpt($opt, $default = null)
3018da7d805SAndreas Gohr    {
3028da7d805SAndreas Gohr        $value = $this->queryValue("SELECT val FROM opts WHERE opt = ?", [$opt]);
303c9d29defSAndreas Gohr        if ($value === null) {
304c9d29defSAndreas Gohr            return $default;
305c9d29defSAndreas Gohr        }
3068da7d805SAndreas Gohr        return $value;
3078da7d805SAndreas Gohr    }
3088da7d805SAndreas Gohr
3098da7d805SAndreas Gohr    /**
3108da7d805SAndreas Gohr     * Set a config value in the opt table
3118da7d805SAndreas Gohr     *
3128da7d805SAndreas Gohr     * @param $opt
3138da7d805SAndreas Gohr     * @param $value
3148da7d805SAndreas Gohr     * @throws \PDOException
3158da7d805SAndreas Gohr     */
3168da7d805SAndreas Gohr    public function setOpt($opt, $value)
3178da7d805SAndreas Gohr    {
3188da7d805SAndreas Gohr        $this->exec('REPLACE INTO opts (opt,val) VALUES (?,?)', [$opt, $value]);
3198da7d805SAndreas Gohr    }
3208da7d805SAndreas Gohr
3218da7d805SAndreas Gohr    /**
3228da7d805SAndreas Gohr     * @return string
3238da7d805SAndreas Gohr     */
3248da7d805SAndreas Gohr    public function getDbName()
3258da7d805SAndreas Gohr    {
3268da7d805SAndreas Gohr        return $this->dbname;
3278da7d805SAndreas Gohr    }
3288da7d805SAndreas Gohr
3298da7d805SAndreas Gohr    /**
3308da7d805SAndreas Gohr     * @return string
3318da7d805SAndreas Gohr     */
3328da7d805SAndreas Gohr    public function getDbFile()
3338da7d805SAndreas Gohr    {
3348da7d805SAndreas Gohr        global $conf;
3358da7d805SAndreas Gohr        return $conf['metadir'] . '/' . $this->dbname . self::FILE_EXTENSION;
3368da7d805SAndreas Gohr    }
3378da7d805SAndreas Gohr
3388da7d805SAndreas Gohr    /**
3398da7d805SAndreas Gohr     * Create a dump of the database and its contents
3408da7d805SAndreas Gohr     *
3418da7d805SAndreas Gohr     * @return string
3428da7d805SAndreas Gohr     * @throws \Exception
3438da7d805SAndreas Gohr     */
3448da7d805SAndreas Gohr    public function dumpToFile($filename)
3458da7d805SAndreas Gohr    {
3468da7d805SAndreas Gohr        $fp = fopen($filename, 'w');
3478da7d805SAndreas Gohr        if (!$fp) {
3488da7d805SAndreas Gohr            throw new \Exception('Could not open file ' . $filename . ' for writing');
3498da7d805SAndreas Gohr        }
3508da7d805SAndreas Gohr
3517ddaad11SAndreas Gohr        $tables = $this->queryAll("SELECT name,sql FROM sqlite_master WHERE type='table'");
3527ddaad11SAndreas Gohr        $indexes = $this->queryAll("SELECT name,sql FROM sqlite_master WHERE type='index'");
3538da7d805SAndreas Gohr
3548da7d805SAndreas Gohr        foreach ($tables as $table) {
3557ddaad11SAndreas Gohr            fwrite($fp, "DROP TABLE IF EXISTS '{$table['name']}';\n");
3567ddaad11SAndreas Gohr        }
3578da7d805SAndreas Gohr
3587ddaad11SAndreas Gohr        foreach ($tables as $table) {
3597ddaad11SAndreas Gohr            fwrite($fp, $table['sql'] . ";\n");
3607ddaad11SAndreas Gohr        }
3617ddaad11SAndreas Gohr
3627ddaad11SAndreas Gohr        foreach ($tables as $table) {
3638da7d805SAndreas Gohr            $sql = "SELECT * FROM " . $table['name'];
3648da7d805SAndreas Gohr            $res = $this->query($sql);
3658da7d805SAndreas Gohr            while ($row = $res->fetch(\PDO::FETCH_ASSOC)) {
3667ddaad11SAndreas Gohr                $values = join(',', array_map(function ($value) {
3677ddaad11SAndreas Gohr                    if ($value === null) return 'NULL';
3687ddaad11SAndreas Gohr                    return $this->pdo->quote($value);
3697ddaad11SAndreas Gohr                }, $row));
3707ddaad11SAndreas Gohr                fwrite($fp, "INSERT INTO '{$table['name']}' VALUES ({$values});\n");
3718da7d805SAndreas Gohr            }
3728da7d805SAndreas Gohr            $res->closeCursor();
3738da7d805SAndreas Gohr        }
3748da7d805SAndreas Gohr
3758da7d805SAndreas Gohr        foreach ($indexes as $index) {
3768da7d805SAndreas Gohr            fwrite($fp, $index['sql'] . ";\n");
3778da7d805SAndreas Gohr        }
3788da7d805SAndreas Gohr        fclose($fp);
3798da7d805SAndreas Gohr        return $filename;
3808da7d805SAndreas Gohr    }
3818da7d805SAndreas Gohr
3828da7d805SAndreas Gohr    // endregion
3838da7d805SAndreas Gohr
3848da7d805SAndreas Gohr    // region migration handling
3858da7d805SAndreas Gohr
3868da7d805SAndreas Gohr    /**
3878da7d805SAndreas Gohr     * Apply all pending migrations
3888da7d805SAndreas Gohr     *
3898da7d805SAndreas Gohr     * Each migration is executed in a transaction which is rolled back on failure
3908da7d805SAndreas Gohr     * Migrations can be files in the schema directory or event handlers
3918da7d805SAndreas Gohr     *
3928da7d805SAndreas Gohr     * @throws \Exception
3938da7d805SAndreas Gohr     */
3948da7d805SAndreas Gohr    protected function applyMigrations()
3958da7d805SAndreas Gohr    {
3968da7d805SAndreas Gohr        $currentVersion = $this->currentDbVersion();
3978da7d805SAndreas Gohr        $latestVersion = $this->latestDbVersion();
3988da7d805SAndreas Gohr
399c70cffc9SAndreas Gohr        if ($currentVersion === $latestVersion) return;
400c70cffc9SAndreas Gohr
4018da7d805SAndreas Gohr        for ($newVersion = $currentVersion + 1; $newVersion <= $latestVersion; $newVersion++) {
4028da7d805SAndreas Gohr            $data = [
4038da7d805SAndreas Gohr                'dbname' => $this->dbname,
4048da7d805SAndreas Gohr                'from' => $currentVersion,
4058da7d805SAndreas Gohr                'to' => $newVersion,
4068da7d805SAndreas Gohr                'file' => $this->getMigrationFile($newVersion),
4078da7d805SAndreas Gohr                'sqlite' => $this->helper,
4088da7d805SAndreas Gohr                'adapter' => $this,
4098da7d805SAndreas Gohr            ];
4108da7d805SAndreas Gohr            $event = new \Doku_Event('PLUGIN_SQLITE_DATABASE_UPGRADE', $data);
4118da7d805SAndreas Gohr
4128da7d805SAndreas Gohr            $this->pdo->beginTransaction();
4138da7d805SAndreas Gohr            try {
4148da7d805SAndreas Gohr                if ($event->advise_before()) {
4158da7d805SAndreas Gohr                    // standard migration file
4167ddaad11SAndreas Gohr                    $sql = Tools::SQLstring2array(file_get_contents($data['file']));
4177ddaad11SAndreas Gohr                    foreach ($sql as $query) {
4187ddaad11SAndreas Gohr                        $this->pdo->exec($query);
4197ddaad11SAndreas Gohr                    }
420c9d29defSAndreas Gohr                } else {
421c9d29defSAndreas Gohr                    if (!$event->result) {
4228da7d805SAndreas Gohr                        // advise before returned false, but the result was false
4238da7d805SAndreas Gohr                        throw new \PDOException('Plugin event did not signal success');
4248da7d805SAndreas Gohr                    }
425c9d29defSAndreas Gohr                }
4268da7d805SAndreas Gohr                $this->setOpt('dbversion', $newVersion);
4278da7d805SAndreas Gohr                $this->pdo->commit();
4288da7d805SAndreas Gohr                $event->advise_after();
4298da7d805SAndreas Gohr            } catch (\Exception $e) {
4308da7d805SAndreas Gohr                // something went wrong, rollback
4318da7d805SAndreas Gohr                $this->pdo->rollBack();
4328da7d805SAndreas Gohr                throw $e;
4338da7d805SAndreas Gohr            }
4348da7d805SAndreas Gohr        }
4358da7d805SAndreas Gohr
4368da7d805SAndreas Gohr        // vacuum the database to free up unused space
4378da7d805SAndreas Gohr        $this->pdo->exec('VACUUM');
4388da7d805SAndreas Gohr    }
4398da7d805SAndreas Gohr
4408da7d805SAndreas Gohr    /**
4418da7d805SAndreas Gohr     * Read the current version from the opt table
4428da7d805SAndreas Gohr     *
4438da7d805SAndreas Gohr     * The opt table is created here if not found
4448da7d805SAndreas Gohr     *
4458da7d805SAndreas Gohr     * @return int
4468da7d805SAndreas Gohr     * @throws \PDOException
4478da7d805SAndreas Gohr     */
4488da7d805SAndreas Gohr    protected function currentDbVersion()
4498da7d805SAndreas Gohr    {
4508da7d805SAndreas Gohr        try {
4518da7d805SAndreas Gohr            $version = $this->getOpt('dbversion', 0);
4528da7d805SAndreas Gohr            return (int)$version;
453*e49844fbSAndreas Gohr        } catch (\PDOException $e) {
454*e49844fbSAndreas Gohr            // temporary logging for #80
455*e49844fbSAndreas Gohr            Logger::error(
456*e49844fbSAndreas Gohr                'SQLite: Could not read dbversion from opt table. Should only happen on new plugin install',
457*e49844fbSAndreas Gohr                [
458*e49844fbSAndreas Gohr                    'dbname' => $this->dbname,
459*e49844fbSAndreas Gohr                    'exception' => get_class($e),
460*e49844fbSAndreas Gohr                    'message' => $e->getMessage(),
461*e49844fbSAndreas Gohr                    'code' => $e->getCode(),
462*e49844fbSAndreas Gohr                ],
463*e49844fbSAndreas Gohr                __FILE__,
464*e49844fbSAndreas Gohr                __LINE__
465*e49844fbSAndreas Gohr            );
466*e49844fbSAndreas Gohr
4678da7d805SAndreas Gohr            // add the opt table - if this fails too, let the exception bubble up
4688da7d805SAndreas Gohr            $sql = "CREATE TABLE IF NOT EXISTS opts (opt TEXT NOT NULL PRIMARY KEY, val NOT NULL DEFAULT '')";
4698da7d805SAndreas Gohr            $this->exec($sql);
4708da7d805SAndreas Gohr            return 0;
4718da7d805SAndreas Gohr        }
4728da7d805SAndreas Gohr    }
4738da7d805SAndreas Gohr
4748da7d805SAndreas Gohr    /**
4758da7d805SAndreas Gohr     * Get the version this db should have
4768da7d805SAndreas Gohr     *
4778da7d805SAndreas Gohr     * @return int
4788da7d805SAndreas Gohr     * @throws \PDOException
4798da7d805SAndreas Gohr     */
4808da7d805SAndreas Gohr    protected function latestDbVersion()
4818da7d805SAndreas Gohr    {
4828da7d805SAndreas Gohr        if (!file_exists($this->schemadir . '/latest.version')) {
4838da7d805SAndreas Gohr            throw new \PDOException('No latest.version in schema dir');
4848da7d805SAndreas Gohr        }
4858da7d805SAndreas Gohr        return (int)trim(file_get_contents($this->schemadir . '/latest.version'));
4868da7d805SAndreas Gohr    }
4878da7d805SAndreas Gohr
4888da7d805SAndreas Gohr    /**
4898da7d805SAndreas Gohr     * Get the migrartion file for the given version
4908da7d805SAndreas Gohr     *
4918da7d805SAndreas Gohr     * @param int $version
4928da7d805SAndreas Gohr     * @return string
4938da7d805SAndreas Gohr     */
4948da7d805SAndreas Gohr    protected function getMigrationFile($version)
4958da7d805SAndreas Gohr    {
4968da7d805SAndreas Gohr        return sprintf($this->schemadir . '/update%04d.sql', $version);
4978da7d805SAndreas Gohr    }
4988da7d805SAndreas Gohr    // endregion
4998da7d805SAndreas Gohr}
500