~aleteoryx/muditaos

334b272fee5827366ccd0696090efa505a6dcba9 — Mateusz Piesta 3 years ago 055a1ca
[MOS-802] Create DB migration script

Added database migration script(LUA).
A scripts/lua/migration/doc/index.html => scripts/lua/migration/doc/index.html +244 -0
@@ 0,0 1,244 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<head>
    <title>Reference</title>
    <link rel="stylesheet" href="ldoc.css" type="text/css" />
</head>
<body>

<div id="container">

<div id="product">
	<div id="product_logo"></div>
	<div id="product_name"><big><b></b></big></div>
	<div id="product_description"></div>
</div> <!-- id="product" -->


<div id="main">


<!-- Menu -->

<div id="navigation">
<br/>
<h1>ldoc</h1>


<h2>Contents</h2>
<ul>
<li><a href="#Functions">Functions</a></li>
<li><a href="#Tables">Tables</a></li>
</ul>


<h2>Modules</h2>
<ul class="nowrap">
  <li><strong>migration</strong></li>
</ul>

</div>

<div id="content">

<h1>Module <code>migration</code></h1>
<p>Database migration scripts</p>
<p></p>


<h2><a href="#Functions">Functions</a></h2>
<table class="function_list">
	<tr>
	<td class="name" nowrap><a href="#up">up (db_path, scripts, target_version)</a></td>
	<td class="summary">Perform database migration from lower to higher version</td>
	</tr>
	<tr>
	<td class="name" nowrap><a href="#down">down (db_path, scripts, target_version)</a></td>
	<td class="summary">Perform database migration from higher to lower version</td>
	</tr>
	<tr>
	<td class="name" nowrap><a href="#get_db_version">get_db_version (db_path)</a></td>
	<td class="summary">Read the database version</td>
	</tr>
	<tr>
	<td class="name" nowrap><a href="#migrate">migrate (db_dir, scripts_dir, db_set)</a></td>
	<td class="summary">Perform databases migration(up/down) automatically</td>
	</tr>
</table>
<h2><a href="#Tables">Tables</a></h2>
<table class="function_list">
	<tr>
	<td class="name" nowrap><a href="#retcode">retcode</a></td>
	<td class="summary">Return codes</td>
	</tr>
</table>

<br/>
<br/>


    <h2 class="section-header "><a name="Functions"></a>Functions</h2>

    <dl class="function">
    <dt>
    <a name = "up"></a>
    <strong>up (db_path, scripts, target_version)</strong>
    </dt>
    <dd>
    Perform database migration from lower to higher version


    <h3>Parameters:</h3>
    <ul>
        <li><span class="parameter">db_path</span>
         path to the database we want to perform migration on
        </li>
        <li><span class="parameter">scripts</span>
         array of {""}
        </li>
        <li><span class="parameter">target_version</span>
         database version we want to migrate in to
        </li>
    </ul>

    <h3>Returns:</h3>
    <ol>

        <a href="index.html#retcode">retcode</a>
    </ol>




</dd>
    <dt>
    <a name = "down"></a>
    <strong>down (db_path, scripts, target_version)</strong>
    </dt>
    <dd>
    Perform database migration from higher to lower version


    <h3>Parameters:</h3>
    <ul>
        <li><span class="parameter">db_path</span>
         path to the database we want to perform migration on
        </li>
        <li><span class="parameter">scripts</span>
         array of {""}
        </li>
        <li><span class="parameter">target_version</span>
         database version we want to migrate in to
        </li>
    </ul>

    <h3>Returns:</h3>
    <ol>

        <a href="index.html#retcode">retcode</a>
    </ol>




</dd>
    <dt>
    <a name = "get_db_version"></a>
    <strong>get_db_version (db_path)</strong>
    </dt>
    <dd>
    Read the database version


    <h3>Parameters:</h3>
    <ul>
        <li><span class="parameter">db_path</span>
         path to the database
        </li>
    </ul>

    <h3>Returns:</h3>
    <ol>

        database version number
    </ol>




</dd>
    <dt>
    <a name = "migrate"></a>
    <strong>migrate (db_dir, scripts_dir, db_set)</strong>
    </dt>
    <dd>
    Perform databases migration(up/down) automatically


    <h3>Parameters:</h3>
    <ul>
        <li><span class="parameter">db_dir</span>
         location of the databases
        </li>
        <li><span class="parameter">scripts_dir</span>
         location of the database migration scripts
        </li>
        <li><span class="parameter">db_set</span>
         array of {<"database_name"> = <db_target_version>} entries
        </li>
    </ul>

    <h3>Returns:</h3>
    <ol>

        <a href="index.html#retcode">retcode</a>
    </ol>




</dd>
</dl>
    <h2 class="section-header "><a name="Tables"></a>Tables</h2>

    <dl class="function">
    <dt>
    <a name = "retcode"></a>
    <strong>retcode</strong>
    </dt>
    <dd>
    Return codes


    <h3>Fields:</h3>
    <ul>
        <li><span class="parameter">OK</span>

        </li>
        <li><span class="parameter">ALREADY_UP_TO_DATE</span>

        </li>
        <li><span class="parameter">WRONG_TARGET_VERSION</span>

        </li>
    </ul>





</dd>
</dl>


</div> <!-- id="content" -->
</div> <!-- id="main" -->
<div id="about">
<i>generated by <a href="http://github.com/stevedonovan/LDoc">LDoc 1.4.6</a></i>
<i style="float:right;">Last updated 2022-11-24 08:40:54 </i>
</div> <!-- id="about" -->
</div> <!-- id="container" -->
</body>
</html>

A scripts/lua/migration/doc/ldoc.css => scripts/lua/migration/doc/ldoc.css +303 -0
@@ 0,0 1,303 @@
/* BEGIN RESET

Copyright (c) 2010, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.com/yui/license.html
version: 2.8.2r1
*/
html {
    color: #000;
    background: #FFF;
}
body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td {
    margin: 0;
    padding: 0;
}
table {
    border-collapse: collapse;
    border-spacing: 0;
}
fieldset,img {
    border: 0;
}
address,caption,cite,code,dfn,em,strong,th,var,optgroup {
    font-style: inherit;
    font-weight: inherit;
}
del,ins {
    text-decoration: none;
}
li {
    margin-left: 20px;
}
caption,th {
    text-align: left;
}
h1,h2,h3,h4,h5,h6 {
    font-size: 100%;
    font-weight: bold;
}
q:before,q:after {
    content: '';
}
abbr,acronym {
    border: 0;
    font-variant: normal;
}
sup {
    vertical-align: baseline;
}
sub {
    vertical-align: baseline;
}
legend {
    color: #000;
}
input,button,textarea,select,optgroup,option {
    font-family: inherit;
    font-size: inherit;
    font-style: inherit;
    font-weight: inherit;
}
input,button,textarea,select {*font-size:100%;
}
/* END RESET */

body {
    margin-left: 1em;
    margin-right: 1em;
    font-family: arial, helvetica, geneva, sans-serif;
    background-color: #ffffff; margin: 0px;
}

code, tt { font-family: monospace; font-size: 1.1em; }
span.parameter { font-family:monospace; }
span.parameter:after { content:":"; }
span.types:before { content:"("; }
span.types:after { content:")"; }
.type { font-weight: bold; font-style:italic }

body, p, td, th { font-size: .95em; line-height: 1.2em;}

p, ul { margin: 10px 0 0 0px;}

strong { font-weight: bold;}

em { font-style: italic;}

h1 {
    font-size: 1.5em;
    margin: 20px 0 20px 0;
}
h2, h3, h4 { margin: 15px 0 10px 0; }
h2 { font-size: 1.25em; }
h3 { font-size: 1.15em; }
h4 { font-size: 1.06em; }

a:link { font-weight: bold; color: #004080; text-decoration: none; }
a:visited { font-weight: bold; color: #006699; text-decoration: none; }
a:link:hover { text-decoration: underline; }

hr {
    color:#cccccc;
    background: #00007f;
    height: 1px;
}

blockquote { margin-left: 3em; }

ul { list-style-type: disc; }

p.name {
    font-family: "Andale Mono", monospace;
    padding-top: 1em;
}

pre {
    background-color: rgb(245, 245, 245);
    border: 1px solid #C0C0C0; /* silver */
    padding: 10px;
    margin: 10px 0 10px 0;
    overflow: auto;
    font-family: "Andale Mono", monospace;
}

pre.example {
    font-size: .85em;
}

table.index { border: 1px #00007f; }
table.index td { text-align: left; vertical-align: top; }

#container {
    margin-left: 1em;
    margin-right: 1em;
    background-color: #f0f0f0;
}

#product {
    text-align: center;
    border-bottom: 1px solid #cccccc;
    background-color: #ffffff;
}

#product big {
    font-size: 2em;
}

#main {
    background-color: #f0f0f0;
    border-left: 2px solid #cccccc;
}

#navigation {
    float: left;
    width: 14em;
    vertical-align: top;
    background-color: #f0f0f0;
    overflow: visible;
}

#navigation h2 {
    background-color:#e7e7e7;
    font-size:1.1em;
    color:#000000;
    text-align: left;
    padding:0.2em;
    border-top:1px solid #dddddd;
    border-bottom:1px solid #dddddd;
}

#navigation ul
{
    font-size:1em;
    list-style-type: none;
    margin: 1px 1px 10px 1px;
}

#navigation li {
    text-indent: -1em;
    display: block;
    margin: 3px 0px 0px 22px;
}

#navigation li li a {
    margin: 0px 3px 0px -1em;
}

#content {
    margin-left: 14em;
    padding: 1em;
    width: 700px;
    border-left: 2px solid #cccccc;
    border-right: 2px solid #cccccc;
    background-color: #ffffff;
}

#about {
    clear: both;
    padding: 5px;
    border-top: 2px solid #cccccc;
    background-color: #ffffff;
}

@media print {
    body {
        font: 12pt "Times New Roman", "TimeNR", Times, serif;
    }
    a { font-weight: bold; color: #004080; text-decoration: underline; }

    #main {
        background-color: #ffffff;
        border-left: 0px;
    }

    #container {
        margin-left: 2%;
        margin-right: 2%;
        background-color: #ffffff;
    }

    #content {
        padding: 1em;
        background-color: #ffffff;
    }

    #navigation {
        display: none;
    }
    pre.example {
        font-family: "Andale Mono", monospace;
        font-size: 10pt;
        page-break-inside: avoid;
    }
}

table.module_list {
    border-width: 1px;
    border-style: solid;
    border-color: #cccccc;
    border-collapse: collapse;
}
table.module_list td {
    border-width: 1px;
    padding: 3px;
    border-style: solid;
    border-color: #cccccc;
}
table.module_list td.name { background-color: #f0f0f0; min-width: 200px; }
table.module_list td.summary { width: 100%; }


table.function_list {
    border-width: 1px;
    border-style: solid;
    border-color: #cccccc;
    border-collapse: collapse;
}
table.function_list td {
    border-width: 1px;
    padding: 3px;
    border-style: solid;
    border-color: #cccccc;
}
table.function_list td.name { background-color: #f0f0f0; min-width: 200px; }
table.function_list td.summary { width: 100%; }

ul.nowrap {
    overflow:auto;
    white-space:nowrap;
}

dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;}
dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;}
dl.table h3, dl.function h3 {font-size: .95em;}

/* stop sublists from having initial vertical space */
ul ul { margin-top: 0px; }
ol ul { margin-top: 0px; }
ol ol { margin-top: 0px; }
ul ol { margin-top: 0px; }

/* make the target distinct; helps when we're navigating to a function */
a:target + * {
  background-color: #FF9;
}


/* styles for prettification of source */
pre .comment { color: #558817; }
pre .constant { color: #a8660d; }
pre .escape { color: #844631; }
pre .keyword { color: #aa5050; font-weight: bold; }
pre .library { color: #0e7c6b; }
pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; }
pre .string { color: #8080ff; }
pre .number { color: #f8660d; }
pre .operator { color: #2239a8; font-weight: bold; }
pre .preprocessor, pre .prepro { color: #a33243; }
pre .global { color: #800080; }
pre .user-keyword { color: #800080; }
pre .prompt { color: #558817; }
pre .url { color: #272fc2; text-decoration: underline; }


A scripts/lua/migration/migration.lua => scripts/lua/migration/migration.lua +169 -0
@@ 0,0 1,169 @@
-- 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), string.format("file: %s", file))
    assert(db:exec(script) == sqlite3.OK, string.format("script:\n%s\n", script))
    assert(db:exec(string.format("PRAGMA user_version=%u;", version)) == sqlite3.OK,
        string.format("version: %d", 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)
    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)
    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:")
    for name, version in pairs(db_set) do
        print("\tname: " .. name)
        print("\ttarget version: " .. version)
    end
end

local function validate_inputs(migration_dir, db_dir)
    assert(helpers.exists(migration_dir), "Migrations 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
        return migration.down(db_path, scripts_down, target_version)
    end
    if db_version < target_version then
        return migration.up(db_path, scripts_up, target_version)
    end
    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("migrations 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

A scripts/lua/migration/test/install_dependencies.sh => scripts/lua/migration/test/install_dependencies.sh +16 -0
@@ 0,0 1,16 @@
#!/usr/bin/env bash
# Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
# For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

function if_cmd_installed() {
    if ! command -v "$1"  &> /dev/null; then
        echo "$1 command could not be found. Please install it using system package manager"
        exit 1
    fi
}

if_cmd_installed "luarocks"

luarocks install lsqlite3complete 
luarocks install lfs
luarocks install luaunit
\ No newline at end of file

A scripts/lua/migration/test/test.lua => scripts/lua/migration/test/test.lua +142 -0
@@ 0,0 1,142 @@
--Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
--For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

package.path = package.path .. ";../?.lua;../../share/?.lua;../../share/?/?.lua"
local lu = require("luaunit")
local sqlite = require("lsqlite3complete")
local lfs = require("lfs")
local migration = require("migration")

local test_db_schema = [[
    CREATE TABLE IF NOT EXISTS test_1
   (
    _id         INTEGER PRIMARY KEY,
   timestamp   TEXT,
   duration    INTEGER
   );"
   ]]

function spawn_db(name, version, schema)
    os.remove(name)
    local db = sqlite.open(name)
    db:exec(schema)
    db:exec(string.format("PRAGMA user_version=%u;", version))
    db:close()
end

function spawn_script(name, dir, db_name, version, body)
    lfs.mkdir(dir .. "/" .. db_name)
    lfs.mkdir(dir .. "/" .. db_name .. "/" .. version)
    local fd = io.open(dir .. "/" .. db_name .. "/" .. version .. "/" .. name, "w")
    fd:write(body)
    fd:close()
end

function test_migration_up_success()
    scripts = {}
    scripts[1] = "ALTER TABLE test_1 ADD new_column TEXT;"
    scripts[2] = "ALTER TABLE test_1 ADD new_column2 TEXT;"
    scripts[3] = "ALTER TABLE test_1 ADD new_column3 TEXT;"
    scripts[4] = "ALTER TABLE test_1 ADD new_column4 TEXT;"
    scripts[5] = "ALTER TABLE test_1 ADD new_column5 TEXT;"

    --- Migrate from version 0 to 1
    spawn_db("test_1.sql", 0, test_db_schema)
    lu.assertEquals(migration.up("test_1.sql", scripts, 1), migration.retcode.OK)
    lu.assertEquals(migration.get_db_version("test_1.sql"), 1)

    -- Migrate from version 1 to 2
    lu.assertEquals(migration.up("test_1.sql", scripts, 2), migration.retcode.OK)
    lu.assertEquals(migration.get_db_version("test_1.sql"), 2)

    --- Migrate from version 0 to 5
    spawn_db("test_1.sql", 0, test_db_schema)
    lu.assertEquals(migration.up("test_1.sql", scripts, 5), migration.retcode.OK)
    lu.assertEquals(migration.get_db_version("test_1.sql"), 5)
end

function test_migration_up_db_errors()
    scripts = {}

    --- Trigger DB error by trying to operate on empty database
    spawn_db("test_1.sql", 1, "")
    scripts[1] = "ALTER TABLE test_1 ADD new_column TEXT;"
    lu.assertError(migration.up, "test_1.sql", scripts, 2)

    --- Target version set to the same value as the current DB version
    spawn_db("test_1.sql", 1, test_db_schema)
    lu.assertEquals(migration.up("test_1.sql", scripts, 1), migration.retcode.ALREADY_UP_TO_DATE)

    --- Target version set to the lower number than the current DB version
    spawn_db("test_1.sql", 2, test_db_schema)
    lu.assertEquals(migration.up("test_1.sql", scripts, 1), migration.retcode.WRONG_TARGET_VERSION)
end

function test_migration_down_success()
    scripts = {}
    scripts[1] = "ALTER TABLE test_1 ADD new_column1 TEXT;"
    scripts[2] = "ALTER TABLE test_1 ADD new_column2 TEXT;"
    scripts[3] = "ALTER TABLE test_1 ADD new_column3 TEXT;"
    scripts[4] = "ALTER TABLE test_1 ADD new_column4 TEXT;"
    scripts[5] = "ALTER TABLE test_1 ADD new_column5 TEXT;"

    --- Migrate from version 2 to 1
    spawn_db("test_1.sql", 2, test_db_schema)
    lu.assertEquals(migration.down("test_1.sql", scripts, 1), migration.retcode.OK)
    lu.assertEquals(migration.get_db_version("test_1.sql"), 1)

    -- Migrate from version 1 to 0
    lu.assertEquals(migration.down("test_1.sql", scripts, 0), migration.retcode.OK)
    lu.assertEquals(migration.get_db_version("test_1.sql"), 0)

    --- Migrate from version 5 to 0
    spawn_db("test_1.sql", 5, test_db_schema)
    lu.assertEquals(migration.down("test_1.sql", scripts, 0), migration.retcode.OK)
    lu.assertEquals(migration.get_db_version("test_1.sql"), 0)
end

function test_migration_down_errors()
    scripts = {}

    --- Trigger DB error by trying to operate on empty database
    spawn_db("test_1.sql", 2, "")
    scripts[1] = "ALTER TABLE test_1 ADD new_column TEXT;"
    lu.assertError(migration.down, "test_1.sql", scripts, 1)

    --- Target version set to the same value as the current DB version
    spawn_db("test_1.sql", 1, test_db_schema)
    lu.assertEquals(migration.down("test_1.sql", scripts, 1), migration.retcode.ALREADY_UP_TO_DATE)

    --- Target version set to the higher number than the current DB version
    spawn_db("test_1.sql", 2, test_db_schema)
    lu.assertEquals(migration.down("test_1.sql", scripts, 3), migration.retcode.WRONG_TARGET_VERSION)
end

function test_automatic_migration()
    spawn_db("databases/test_1.db", 0, test_db_schema)
    spawn_db("databases/test_2.db", 0, test_db_schema)
    spawn_script("up.sql", "migrations", "test_1", 1, "insert into test_1 (duration) values(100);")
    spawn_script("up.sql", "migrations", "test_1", 2, "insert into test_1 (duration) values(101);")
    spawn_script("up.sql", "migrations", "test_2", 1, "insert into test_1 (duration) values(100);")
    spawn_script("up.sql", "migrations", "test_2", 2, "insert into test_1 (duration) values(101);")
    spawn_script("down.sql", "migrations", "test_1", 1, "delete from test_1 where _id=1;")
    spawn_script("down.sql", "migrations", "test_1", 2, "delete from test_1 where _id=2;")
    spawn_script("down.sql", "migrations", "test_2", 1, "delete from test_1 where _id=1;")
    spawn_script("down.sql", "migrations", "test_2", 2, "delete from test_1 where _id=2;")

    migration.migrate("databases", "migrations", {
        test_1 = 2,
        test_2 = 2
    })
    lu.assertEquals(migration.get_db_version("databases/test_1.db"), 2)
    lu.assertEquals(migration.get_db_version("databases/test_2.db"), 2)

    migration.migrate("databases", "migrations", {
        test_1 = 1,
        test_2 = 1
    })
    lu.assertEquals(migration.get_db_version("databases/test_1.db"), 1)
    lu.assertEquals(migration.get_db_version("databases/test_2.db"), 1)
end

os.exit(lu.LuaUnit.run())