-- Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
-- For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
--- Database migration scripts
-- @module migration
local sqlite3 = require("lsqlite3complete")
local lfs = require("lfs")
local helpers = require("helpers")
local up_script = "up.sql"
local down_script = "down.sql"
migration = {}
--- Return codes
-- @table retcode
migration.retcode = {
OK = 0,
ALREADY_UP_TO_DATE = 1,
WRONG_TARGET_VERSION = 2
}
local function build_script_array(path, db_name, filename)
local scripts = {}
for file in lfs.dir(path .. "/" .. db_name) do
if file ~= "." and file ~= ".." and tonumber(file) then
local f = path .. '/' .. db_name .. '/' .. file .. '/' .. filename
local attr = lfs.attributes(f)
assert(type(attr) == "table")
scripts[tonumber(file)] = helpers.read_whole_file(f)
end
end
return scripts
end
local function build_database_path(path, db_name)
return path .. "/" .. db_name .. ".db"
end
local function db_exec(file, script, version)
local db = assert(sqlite3.open(file))
assert(db:exec(script) == sqlite3.OK, string.format("Script execution failed:\n%s\n", script))
assert(db:exec(string.format("PRAGMA user_version=%u;", version)) == sqlite3.OK,
string.format("Setting database version: %d failed", version))
db:close()
end
local function read_db_version(file)
local db = assert(sqlite3.open(file))
local stmt = assert(db:prepare("PRAGMA user_version;"))
local ret = {}
for v in stmt:urows() do
ret = v
end
db:close()
return ret
end
local function db_migrate_up(db_path, scripts, target_version)
local current_version = read_db_version(db_path)
if current_version == target_version then
return migration.retcode.ALREADY_UP_TO_DATE
end
if current_version > target_version then
return migration.retcode.WRONG_TARGET_VERSION
end
for v = current_version, target_version - 1 do
db_exec(db_path, scripts[v + 1], v + 1)
end
return migration.retcode.OK
end
local function db_migrate_down(db_path, scripts, target_version)
local current_version = read_db_version(db_path)
if current_version == target_version then
return migration.retcode.ALREADY_UP_TO_DATE
end
if current_version < target_version then
return migration.retcode.WRONG_TARGET_VERSION
end
for v = current_version, target_version + 1, -1 do
db_exec(db_path, scripts[v], v - 1)
end
return migration.retcode.OK
end
local function print_db_set(db_set)
print("Database set(name,version):")
for name, version in pairs(db_set) do
print(string.format(" '%s':%d", name, version))
end
end
local function validate_inputs(migration_dir, db_dir)
assert(helpers.exists(migration_dir), "Migration directory does not exist")
assert(helpers.exists(db_dir), "Databases directory does not exist")
return true
end
local function migrate(db_path, scripts_up, scripts_down, target_version)
local db_version = read_db_version(db_path)
if db_version > target_version then
print(string.format("Performing migration-down of '%s' from version %d to %d", db_path, db_version,
target_version))
return migration.down(db_path, scripts_down, target_version)
end
if db_version < target_version then
print(
string.format("Performing migration-up of '%s' from version %d to %d", db_path, db_version, target_version))
return migration.up(db_path, scripts_up, target_version)
end
print(string.format("Migration not needed, '%s' is already the newest version", db_path))
return migration.retcode.OK
end
--- Perform database migration from lower to higher version
-- @function up
-- @param db_path path to the database we want to perform migration on
-- @param scripts array of {""}
-- @param target_version database version we want to migrate in to
-- @return @{retcode}
function migration.up(db_path, scripts, target_version)
return db_migrate_up(db_path, scripts, target_version)
end
--- Perform database migration from higher to lower version
-- @function down
-- @param db_path path to the database we want to perform migration on
-- @param scripts array of {""}
-- @param target_version database version we want to migrate in to
-- @return @{retcode}
function migration.down(db_path, scripts, target_version)
return db_migrate_down(db_path, scripts, target_version)
end
--- Read the database version
-- @function get_db_version
-- @param db_path path to the database
-- @return database version number
function migration.get_db_version(db_path)
return read_db_version(db_path)
end
--- Perform databases migration(up/down) automatically
-- @function migrate
-- @param db_dir location of the databases
-- @param scripts_dir location of the database migration scripts
-- @param db_set array of {<"database_name"> = <db_target_version>} entries
-- @return @{retcode}
function migration.migrate(db_dir, scripts_dir, db_set)
print(string.format("Migration scripts directory: '%s'", scripts_dir))
print(string.format("Databases directory: '%s'", db_dir))
print_db_set(db_set)
for name, version in pairs(db_set) do
local database_path = build_database_path(db_dir, name)
local scripts_up = build_script_array(scripts_dir, name, up_script)
local scripts_down = build_script_array(scripts_dir, name, down_script)
local ret = migrate(database_path, scripts_up, scripts_down, version)
if ret ~= migration.retcode.OK then
return ret
end
end
return migration.retcode.OK
end
return migration