gboudreau/Greyhole

View on GitHub
includes/DB.php

Summary

Maintainability
F
3 days
Test Coverage
<?php
/*
Copyright 2014-2020 Guillaume Boudreau

This file is part of Greyhole.

Greyhole is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Greyhole is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Greyhole.  If not, see <http://www.gnu.org/licenses/>.
*/

// Usage: $DB = new DatabaseHelper($options);

final class DB {

    /** @var stdClass */
    protected static $options; // connection options
    /** @var PDO */
    protected static $handle; // database handle

    /**
     * @return bool
     */
    public static function isConnected() {
        return (bool) self::$handle;
    }

    public static function setOptions($options) {
        self::$options = to_object($options);
    }

    public static function connect($retry_until_successful=FALSE, $throw_exception_on_error=FALSE, $timeout = 10) {
        $connect_string = 'mysql:host=' . self::$options->host . ';dbname=' . self::$options->name . ';charset=utf8mb4';

        try {
            self::$handle = @new PDO($connect_string, self::$options->user, self::$options->pass, array(PDO::ATTR_TIMEOUT => $timeout, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
        } catch (PDOException $ex) {
            if ($retry_until_successful) {
                sleep(2);
                return DB::connect(TRUE);
            }
            if ($throw_exception_on_error) {
                throw new Exception("Can't connect to database: " . $ex->getMessage(), $ex->getCode(), $ex);
            } else {
                echo "ERROR: Can't connect to database: " . $ex->getMessage() . "\n";
                Log::critical("Can't connect to database: " . $ex->getMessage(), Log::EVENT_CODE_DB_CONNECT_FAILED);
            }
        }

        if (self::$handle) {
            DB::execute("SET SESSION group_concat_max_len = 1048576");
            DB::execute("SET SESSION wait_timeout = 86400"); # Allow 24h fsck!
            if (self::$options->name != 'mysql') {
                DB::migrate();
            }

            $now = DB::getFirstValue("SELECT NOW()");
            $diff = time() - strtotime($now);
            if (abs($diff) > 20*60) {
                $symbol = $diff < 0 ? '-' : '+';
                $diff_minutes = round(abs($diff)/60);
                $diff_hours = floor($diff_minutes/60);
                $diff_minutes -= $diff_hours*60;
                $mysql_tz = sprintf("%s%02d:%02d", $symbol, $diff_hours, $diff_minutes);
                Log::info("Adjusting MySQL Timezone: $diff secs difference between MySQL and PHP => Changing MySQL TZ to '$mysql_tz'");
                try {
                    DB::execute("SET time_zone = :tz", ['tz' => $mysql_tz]);
                } catch (Exception $ex) {
                    Log::error("Tried to change MySQL's timezone to $mysql_tz, since the system's TZ and MySQL's TZ don't match, and that failed. Error: " . $ex->getMessage() . " To fix this issue, change either the system's or MySQL's timezone so that both match.", Log::EVENT_CODE_DB_TZ_CHANGE_FAILED);
                }
            }
        }

        return self::$handle;
    }

    public static function execute($q, $args = array(), $attempt_repair=TRUE) {
        $stmt = self::$handle->prepare($q);
        foreach ($args as $key => $value) {
            $stmt->bindValue(":$key", $value);
        }
        try {
            $stmt->execute();
            return $stmt;
        } catch (PDOException $e) {
            $error = $e->errorInfo;
            if (($error[1] == 144 || $error[1] == 145) && $attempt_repair) {
                Log::info("Error during MySQL query: " . $e->getMessage() . '. Will now try to repair the MySQL tables.');
                DB::repairTables();
                return DB::execute($q, $args, FALSE); // $attempt_repair = FALSE, to not go into an infinite loop, if the repair doesn't work.
            }
            if ($error[1] == 1406 && $attempt_repair) {
                Log::info("Error during MySQL query: " . $e->getMessage() . '. Will now try to use larger full_path columns.');
                DB::migrate_large_fullpath();
                return DB::execute($q, $args, FALSE); // $attempt_repair = FALSE, to not go into an infinite loop, if the fix doesn't work.
            }
            throw new Exception($e->getMessage(), $error[1]);
        }
    }

    public static function insert($q, $args = array()) {
        if (DB::execute($q, $args) === FALSE) {
            return FALSE;
        }
        return DB::lastInsertedId();
    }

    public static function getFirst($q, $args = array()) {
        $stmt = DB::execute($q, $args);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($result === FALSE) {
            return FALSE;
        }
        return (object) $result;
    }

    public static function getFirstValue($q, $args = array()) {
        $row = DB::getFirst($q, $args);
        if (empty($row)) {
            return FALSE;
        }
        $row = (array) $row;
        return array_shift($row);
    }

    public static function getAll($q, $args = array(), $index_field=NULL) {
        $stmt = DB::execute($q, $args);
        $rows = array();
        $i = 0;
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $index = $i++;
            if (!empty($index_field)) {
                $index = $row[$index_field];
            }
            $rows[$index] = (object) $row;
        }
        return $rows;
    }

    public static function getAllValues($q, $args = array(), $data_type=null) {
        $stmt = DB::execute($q, $args);
        $values = array();
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            if (!is_array($row)) {
                return FALSE;
            }

            $value = array_shift($row);
            if (!empty($data_type)) {
                settype($value, $data_type);
            }
            $values[] = $value;
        }
        return $values;
    }

    public static function lastInsertedId() {
        $q = "SELECT LAST_INSERT_ID()";
        $lastInsertedId = (int) DB::getFirstValue($q);
        if ($lastInsertedId === 0) {
            return TRUE;
        }
        return $lastInsertedId;
    }

    public static function acquireLock($name, $timeout = NULL) {
        $locked = static::getFirstValue("SELECT GET_LOCK(:name, :timeout)", ['name' => $name, 'timeout' => $timeout]);
        if ($locked) {
            return TRUE;
        }
        return FALSE;
    }

    public static function releaseLock($name) {
        $released = static::getFirstValue("SELECT RELEASE_LOCK(:name)", ['name' => $name]);
        if (!$released && static::isLocked($name)) {
            return FALSE;
        }
        return TRUE;
    }

    public static function isLocked($name) {
        $is_lock_free = static::getFirstValue("SELECT IS_FREE_LOCK(:name)", $name);
        return !$is_lock_free;
    }

    public static function error() {
        return self::$options->error;
    }

    private static function migrate() {
        $db_version = (int) Settings::get('db_version');
        if ($db_version < 11) {
            DB::migrate_1_frozen_thaw();
            DB::migrate_2_idle();
            DB::migrate_3_larger_settings();
            DB::migrate_4_find_next_task_index();
            DB::migrate_5_find_next_task_index();
            DB::migrate_6_md5_worker_indexes();
            DB::migrate_7_larger_full_path();
            DB::migrate_8_du_stats();
            DB::migrate_9_complete_writen();
            DB::migrate_10_utf8();
            DB::migrate_11_varchar();
        }
        if ($db_version < 12) {
            DB::migrate_12_force_update_complete();
            Settings::set('db_version', 12);
        }
        if ($db_version < 13) {
            DB::migrate_13_checksums();
            Settings::set('db_version', 13);
        }
        if ($db_version < 14) {
            DB::migrate_14_status();
            Settings::set('db_version', 14);
        }
        if ($db_version < 15) {
            DB::migrate_15_status_myisam();
            Settings::set('db_version', 15);
        }
        if ($db_version < 16) {
            DB::migrate_16_larger_action();
            Settings::set('db_version', 16);
        }
        if ($db_version < 17) {
            DB::migrate_17_status_action();
            Settings::set('db_version', 17);
        }
        if ($db_version < 18) {
            DB::migrate_18_full_path_utf8mb4();
            Settings::set('db_version', 18);
        }
    }

    // Migration #1 (complete = frozen|thawed)
    private static function migrate_1_frozen_thaw() {
        // Deprecated by migration #9
        // DB::execute("ALTER TABLE tasks CHANGE complete complete ENUM('yes','no','frozen','thawed') NOT NULL");
        // DB::execute("ALTER TABLE tasks_completed CHANGE complete complete ENUM('yes','no','frozen','thawed') NOT NULL");
    }

    // Migration #2 (complete = idle)
    private static function migrate_2_idle() {
        // Deprecated by migration #9
        // DB::execute("ALTER TABLE tasks CHANGE complete complete ENUM('yes','no','frozen','thawed','idle') NOT NULL");
        // DB::execute("ALTER TABLE tasks_completed CHANGE complete complete ENUM('yes','no','frozen','thawed','idle') NOT NULL");
    }

    // Migration #3 (larger settings.value: tinytext > text)
    private static function migrate_3_larger_settings() {
        $query = "DESCRIBE settings";
        $rows = DB::getAll($query);
        foreach ($rows as $row) {
            if ($row->Field == 'value') {
                if ($row->Type == "tinytext") {
                    // migrate
                    DB::execute("ALTER TABLE settings CHANGE value value TEXT CHARACTER SET utf8 NOT NULL");
                }
                break;
            }
        }
    }

    // Migration #4 (new index for find_next_task function, used by DBSpool::execute_next_task() function; also remove deprecated indexes)
    private static function migrate_4_find_next_task_index() {
        $query = "SHOW INDEX FROM tasks WHERE Key_name = 'find_next_task'";
        $row = DB::getFirst($query);
        if ($row === FALSE) {
            // migrate
            DB::execute("ALTER TABLE tasks ADD INDEX find_next_task (complete, share(64), id)");
        }

        $query = "SHOW INDEX FROM tasks WHERE Key_name = 'incomplete_open'";
        $row = DB::getFirst($query);
        if ($row) {
            // migrate
            DB::execute("ALTER TABLE tasks DROP INDEX incomplete_open");
        }

        $query = "SHOW INDEX FROM tasks WHERE Key_name = 'subsequent_writes'";
        $row = DB::getFirst($query);
        if ($row) {
            // migrate
            DB::execute("ALTER TABLE tasks DROP INDEX subsequent_writes");
        }

        $query = "SHOW INDEX FROM tasks WHERE Key_name = 'unneeded_unlinks'";
        $row = DB::getFirst($query);
        if ($row) {
            // migrate
            DB::execute("ALTER TABLE tasks DROP INDEX unneeded_unlinks");
        }
    }

    // Migration #5 (fix find_next_task index)
    private static function migrate_5_find_next_task_index() {
        $query = "SHOW INDEX FROM tasks WHERE Key_name = 'find_next_task' and Column_name = 'share'";
        $row = DB::getFirst($query);
        if ($row !== FALSE) {
            // migrate
            DB::execute("ALTER TABLE tasks DROP INDEX find_next_task");
            DB::execute("ALTER TABLE tasks ADD INDEX find_next_task (complete, id)");
        }
    }

    // Migration #6 (new indexes for md5_worker_thread/gh_check_md5 functions)
    private static function migrate_6_md5_worker_indexes() {
        $query = "SHOW INDEX FROM tasks WHERE Key_name = 'md5_worker'";
        $row = DB::getFirst($query);
        if ($row === FALSE) {
            // migrate
            DB::execute("ALTER TABLE tasks ADD INDEX md5_worker (action, complete, additional_info(100), id)");
        }

        $query = "SHOW INDEX FROM tasks WHERE Key_name = 'md5_checker'";
        $row = DB::getFirst($query);
        if ($row === FALSE) {
            // migrate
            DB::execute("ALTER TABLE tasks ADD INDEX md5_checker (action, share(64), full_path(265), complete)");
        }

        $query = "DESCRIBE tasks";
        $rows = DB::getAll($query);
        foreach ($rows as $row) {
            if ($row->Field == 'additional_info') {
                if ($row->Type == "tinytext") {
                    // migrate
                    DB::execute("ALTER TABLE tasks CHANGE additional_info additional_info TEXT CHARACTER SET utf8 NULL");
                }
                break;
            }
        }
    }

    // Migration #7 (full_path new size: 4096)
    private static function migrate_7_larger_full_path() {
        // Deprecated by migration #11
        // DB::execute("ALTER TABLE tasks CHANGE full_path full_path TEXT CHARACTER SET utf8 NULL");
        // DB::execute("ALTER TABLE tasks_completed CHANGE full_path full_path TEXT CHARACTER SET utf8 NULL");
    }

    // Migration #8 (new du_stats table)
    private static function migrate_8_du_stats() {
        $query = "CREATE TABLE IF NOT EXISTS `du_stats` (`share` TINYTEXT NOT NULL, `full_path` TEXT NOT NULL, `depth` TINYINT(3) UNSIGNED NOT NULL, `size` BIGINT(20) UNSIGNED NOT NULL, UNIQUE KEY `uniqness` (`share`(64),`full_path`(269))) ENGINE = MYISAM DEFAULT CHARSET=utf8";
        DB::execute($query);
        $query = "SHOW INDEX FROM `du_stats` WHERE Key_name = 'uniqness'";
        $row = DB::getFirst($query);
        if ($row === FALSE) {
            // migrate
            DB::execute("TRUNCATE `du_stats`");
            DB::execute("ALTER TABLE `du_stats` ADD UNIQUE `uniqness` (`share` (64), `full_path` (269))");
        }
    }

    // Migration #9 (complete = written)
    private static function migrate_9_complete_writen() {
        $query = "DESCRIBE tasks";
        $rows = DB::getAll($query);
        foreach ($rows as $row) {
            if ($row->Field == 'complete') {
                if ($row->Type == "enum('yes','no','frozen','thawed','idle')") {
                    // migrate
                    DB::execute("ALTER TABLE tasks CHANGE complete complete ENUM('yes','no','frozen','thawed','idle','written') NOT NULL");
                    DB::execute("ALTER TABLE tasks_completed CHANGE complete complete ENUM('yes','no','frozen','thawed','idle','written') NOT NULL");
                }
                break;
            }
        }
    }

    // Migration #10 (preparing du_stats `uniqness` index for UTF8 (was too large before)
    // Migration #10 (preparing tasks `md5_checker` index for UTF8 (was too large before)
    // Migration #10 (correct UTF8 columns and tables!)
    private static function migrate_10_utf8() {
        $q = "SHOW INDEX FROM du_stats WHERE key_name = 'uniqness' AND column_name = 'full_path'";
        $index_def = DB::getFirst($q);
        if ($index_def->Sub_part > 269) {
            $q = "ALTER TABLE du_stats DROP INDEX uniqness";
            DB::execute($q);
            $q = "ALTER TABLE du_stats ADD UNIQUE INDEX `uniqness` (share(64), full_path(269))";
            DB::execute($q);
        }

        $q = "SHOW INDEX FROM tasks WHERE key_name = 'md5_checker' AND column_name = 'full_path'";
        $index_def = DB::getFirst($q);
        if ($index_def->Sub_part > 265) {
            $q = "ALTER TABLE tasks DROP INDEX md5_checker";
            DB::execute($q);
            $q = "ALTER TABLE tasks ADD INDEX `md5_checker` (action, share(64), full_path(265), complete)";
            DB::execute($q);
        }

        $tables = array(
            'du_stats',
            'settings',
            'tasks',
            'tasks_completed'
        );
        $columns = array(
            'du_stats|share|TINYTEXT CHARACTER SET utf8 NOT NULL',
            'du_stats|full_path|TEXT CHARACTER SET utf8 NOT NULL',
            'settings|name|TINYTEXT CHARACTER SET utf8 NOT NULL',
            'settings|value|TEXT CHARACTER SET utf8 NOT NULL',
            'tasks|share|TINYTEXT CHARACTER SET utf8 NOT NULL',
            'tasks|full_path|TEXT CHARACTER SET utf8 NULL',
            'tasks|additional_info|TEXT CHARACTER SET utf8 NULL',
            'tasks_completed|share|TINYTEXT CHARACTER SET utf8 NOT NULL',
            'tasks_completed|full_path|TEXT CHARACTER SET utf8 NULL',
            'tasks_completed|additional_info|TEXT CHARACTER SET utf8 NULL',
        );

        $query = "SELECT CCSA.character_set_name FROM information_schema.`TABLES` T, information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` CCSA WHERE CCSA.collation_name = T.table_collation AND T.table_schema = :schema AND T.table_name = :table";
        foreach ($tables as $table_name) {
            $charset = DB::getFirstValue($query, array('schema' => Config::get(CONFIG_DB_NAME), 'table' => $table_name));
            if ($charset != "utf8") {
                Log::info("Updating $table_name table to UTF-8");
                try {
                    DB::execute("ALTER TABLE `$table_name` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci");
                } catch (Exception $ex) {
                    try {
                        DB::execute("ALTER TABLE `$table_name` CHARACTER SET utf8 COLLATE utf8_general_ci");
                    } catch (Exception $ex) {
                        Log::warn("  ALTER TABLE failed.", Log::EVENT_CODE_DB_MIGRATION_FAILED);
                    }
                }
            }
        }

        $query = "SELECT character_set_name FROM information_schema.`COLUMNS` C WHERE table_schema = :schema AND table_name = :table AND column_name = :field";
        foreach ($columns as $value) {
            list($table_name, $column_name, $definition) = explode('|', $value);
            $charset = DB::getFirstValue($query, array('schema' => Config::get(CONFIG_DB_NAME), 'table' => $table_name, 'field' => $column_name));
            if ($charset != "utf8") {
                Log::info("Updating $table_name.$column_name column to UTF-8");
                DB::execute("ALTER TABLE `$table_name` CHANGE `$column_name` `$column_name` $definition");
            }
        }
    }

    // Migration #11: TINYTEXT > VARCHAR(255)
    private static function migrate_11_varchar() {
        $q = "ALTER TABLE `settings` CHANGE `name` `name` VARCHAR(255) NOT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` DROP INDEX `md5_checker`";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` CHANGE `share` `share` VARCHAR(255) NOT NULL, CHANGE `full_path` `full_path` VARCHAR(255) NULL, CHANGE `additional_info` `additional_info` VARCHAR(255) NULL";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` ADD INDEX `md5_checker` (`action`, `share`(64), `full_path`, `complete`)";
        DB::execute($q);
        $q = "ALTER TABLE `tasks_completed` CHANGE `share` `share` VARCHAR(255) NOT NULL, CHANGE `full_path` `full_path` VARCHAR(255) NULL, CHANGE `additional_info` `additional_info` VARCHAR(255) NULL";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` DROP INDEX `uniqness`";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` CHANGE `share` `share` VARCHAR(255) NOT NULL, CHANGE `full_path` `full_path` VARCHAR(255) NOT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` ADD UNIQUE KEY `uniqness` (`share`(64),`full_path`)";
        DB::execute($q);
    }

    // Migration #12 (unconditional change of complete columns)
    private static function migrate_12_force_update_complete() {
        DB::execute("ALTER TABLE tasks CHANGE complete complete ENUM('yes','no','frozen','thawed','idle','written') CHARACTER SET ascii NOT NULL");
        DB::execute("ALTER TABLE tasks_completed CHANGE complete complete ENUM('yes','no','frozen','thawed','idle','written') CHARACTER SET ascii NOT NULL");
    }

    // For users who deal with full_path > 255 characters, migrate to large TEXT fields
    private static function migrate_large_fullpath() {
        $q = "ALTER TABLE `settings` DROP PRIMARY KEY";
        DB::execute($q);
        $q = "ALTER TABLE `settings` CHANGE `name` `name` TEXT NOT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `settings` ADD PRIMARY KEY (`name`(255))";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` DROP INDEX `md5_checker`";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` CHANGE `full_path` `full_path` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `additional_info` `additional_info` TEXT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` ADD INDEX `md5_checker` (`action`, `share`(64), `full_path`(180), `complete`)";
        DB::execute($q);
        $q = "ALTER TABLE `tasks_completed` CHANGE `full_path` `full_path` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `additional_info` `additional_info` TEXT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` DROP INDEX `uniqness`";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` CHANGE `full_path` `full_path` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` ADD UNIQUE KEY `uniqness` (`share`(64),`full_path`(200))";
        DB::execute($q);
    }

    private static function migrate_13_checksums() {
        $query = "CREATE TABLE IF NOT EXISTS `checksums` (`id` char(32) NOT NULL DEFAULT '', `share` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', `full_path` text CHARACTER SET utf8 NOT NULL, `checksum` char(32) NOT NULL DEFAULT '', `last_checked` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), PRIMARY KEY (`id`)) ENGINE = MYISAM DEFAULT CHARSET=ascii";
        DB::execute($query);
    }

    private static function migrate_14_status() {
        $query = "CREATE TABLE IF NOT EXISTS `status` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,`date_time` timestamp NOT NULL DEFAULT current_timestamp(),`action` enum('initialize','unknown','daemon','pause','resume','fsck','balance','stats','status','logs','trash','queue','iostat','getuid','worker','symlinks','replace','for','gone','going','thaw','debug','metadata','share','check_pool','sleep','read_smb_spool','fsck_file') DEFAULT NULL,`log` text NOT NULL,UNIQUE KEY `id` (`id`)) ENGINE=MYISAM DEFAULT CHARSET=utf8";
        DB::execute($query);
    }

    private static function migrate_15_status_myisam() {
        $query = "ALTER TABLE `status` ENGINE = MYISAM";
        DB::execute($query);
    }

    private static function migrate_16_larger_action() {
        $query = "ALTER TABLE `tasks` CHANGE `action` `action` varchar(12) CHARACTER SET ascii NOT NULL DEFAULT ''";
        DB::execute($query);
        $query = "ALTER TABLE `tasks_completed` CHANGE `action` `action` varchar(12) CHARACTER SET ascii NOT NULL DEFAULT ''";
        DB::execute($query);
    }

    private static function migrate_17_status_action() {
        $query = "ALTER TABLE `status` CHANGE `action` `action` varchar(16) CHARACTER SET latin1 COLLATE latin1_general_ci DEFAULT NULL";
        DB::execute($query);
    }

    // Migration #18: use utf8mb4 for full_path to handle emoji in file name
    private static function migrate_18_full_path_utf8mb4() {
        $q = "ALTER TABLE `checksums` CHANGE `full_path` `full_path` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL";
        DB::execute($q);

        $query = "DESCRIBE tasks";
        $rows = DB::getAll($query);
        foreach ($rows as $row) {
            if ($row->Field == 'full_path') {
                if ($row->Type == 'text') {
                    // tasks.full_path is using larger columns, TEXT vs VARCHAR(255), so we need to keep using larger columns.
                    // migrate_large_fullpath() will convert those columns to utf8mb4; the code below will only be used for DB using VARCHAR(255) columns.
                    DB::migrate_large_fullpath();
                    return;
                }
                break;
            }
        }

        $q = "ALTER TABLE `tasks` DROP INDEX `md5_checker`";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` CHANGE `full_path` `full_path` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `additional_info` `additional_info` TEXT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` ADD INDEX `md5_checker` (`action`, `share`(64), `full_path`(180), `complete`)";
        DB::execute($q);
        $q = "ALTER TABLE `tasks_completed` CHANGE `full_path` `full_path` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `additional_info` `additional_info` TEXT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` DROP INDEX `uniqness`";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` CHANGE `full_path` `full_path` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` ADD UNIQUE KEY `uniqness` (`share`(64),`full_path`(200))";
        DB::execute($q);
    }

    public static function repairTables() {
        if (Log::actionIs(ACTION_DAEMON)) {
            Log::info("Checking MySQL tables...");
        }
        // Let's repair/optimize tables only if they need to!
        foreach (array('tasks', 'settings', 'du_stats', 'tasks_completed') as $table_name) {
            try {
                DB::execute("SELECT * FROM $table_name LIMIT 1", array(), FALSE);
            } catch (Exception $e) {
                Log::warn("Test failed for $table_name MySQL table: " . $e->getMessage() . " - Will try to repair it using: REPAIR TABLE $table_name ...", Log::EVENT_CODE_DB_TABLE_CRASHED);
                DB::execute("REPAIR TABLE $table_name", array(), FALSE);
            }
        }
    }

    public static function deleteExecutedTasks() {
        $executed_tasks_retention = Config::get(CONFIG_EXECUTED_TASKS_RETENTION);
        if ($executed_tasks_retention == 'forever') {
            return;
        }
        if (!is_int($executed_tasks_retention)) {
            Log::critical("Error: Invalid value for 'executed_tasks_retention' in greyhole.conf: '$executed_tasks_retention'. You need to use either 'forever' (no quotes), or a number of days.", Log::EVENT_CODE_CONFIG_INVALID_VALUE);
        }
        Log::info("Cleaning executed tasks: keeping the last $executed_tasks_retention days of logs.");
        $query = sprintf("DELETE FROM tasks_completed WHERE event_date < NOW() - INTERVAL %d DAY", (int) $executed_tasks_retention);
        DB::execute($query);
    }
}

?>