///|
pub(all) struct Migration {
version : Int32
name : String
statements : Array[String]
}
///|
/// Declare a forward-only migration: its version, a human-readable name and
/// the SQL statements to apply, in order.
pub fn migration(
version : Int32,
name : String,
statements : Array[String],
) -> Migration {
{ version, name, statements, }
}
///|
let migrations_table = "moonway_migrations"
///|
/// Apply pending migrations in ascending version order. Already-applied
/// versions (recorded in the moonway_migrations table) are skipped; the
/// runner stops at the first failing statement and returns false.
pub fn Database::migrate(
self : Database,
migrations : Array[Migration],
) -> Bool {
if !self.exec(
"CREATE TABLE IF NOT EXISTS \"\{migrations_table}\" (\"version\" INTEGER PRIMARY KEY, \"name\" TEXT)",
) {
return false
}
let pending = sorted_pending(migrations, self.applied_versions())
for m in pending {
for statement in m.statements {
if !self.exec(statement) {
return false
}
}
if !self.record_applied(m) {
return false
}
}
true
}
///|
/// Versions recorded as applied, ascending.
fn Database::applied_versions(self : Database) -> Array[Int32] {
let versions = []
let queried = self.inner.query(
"SELECT \"version\" FROM \"\{migrations_table}\" ORDER BY \"version\"",
)
match queried {
Some(stmt) => {
for row in stmt.iter() {
versions.push(row.column_int(0))
}
stmt.finalize()
}
None => ()
}
versions
}
///|
/// Migrations not yet applied, ascending by version.
fn sorted_pending(
migrations : Array[Migration],
applied : Array[Int32],
) -> Array[Migration] {
let pending = []
for m in migrations {
if !applied.contains(m.version) {
pending.push(m)
}
}
insertion_sort_by_version(pending)
pending
}
///|
fn insertion_sort_by_version(migrations : Array[Migration]) -> Unit {
for i in 1..= 0 && migrations[j].version > m.version {
migrations[j + 1] = migrations[j]
j = j - 1
}
migrations[j + 1] = m
}
}
///|
fn Database::record_applied(self : Database, m : Migration) -> Bool {
let prepared = self.inner.prepare(
"INSERT INTO \"\{migrations_table}\" (\"version\", \"name\") VALUES (?, ?)",
)
match prepared {
Some(stmt) => {
stmt.bind_all([@sqlite.Int(m.version), @sqlite.Text(encode_utf8(m.name))])
|> ignore
let executed = stmt.execute()
stmt.finalize()
executed
}
None => false
}
}