///|
/// Versioned schema migrations (Alembic / Diesel-migrations): a `Migration` bundles a
/// version with its forward (`up`) and reverse (`down`) statements; a `Migrator` records
/// applied versions in a bookkeeping table and drives the schema forward (`up`, applying
/// only what is pending — idempotent) or back (`down_to`). `current_version` and
/// `applied_versions` report state. Everything travels through the `@moondb.Driver` seam,
/// so the same Migrator runs on any backend. The driver here is a small double modelling
/// the `schema_migrations` bookkeeping table, so the whole example stays backend-free
/// and compiles on every target; against real SQLite/Postgres the Migrator is identical.
///
/// moon run examples/19-migrations
///
/// The bookkeeping table the Migrator maintains, in memory. It interprets exactly the
/// statements `Migrator` issues against `schema_migrations` (defined in migrations.mbt)
/// — the version INSERT/DELETE and the two version SELECTs — and treats every other
/// statement (the migrations' own DDL) as an applied no-op it need not model.
struct SchemaLog {
applied : Array[(Int32, String)]
mut closed : Bool
}
///|
fn version_of(v : @moondb.Value) -> Int32 {
match v {
Int(n) => n
Int64(n) => n.to_int()
_ => -1
}
}
///|
fn text_of(v : @moondb.Value) -> String {
match v {
Text(s) => s
_ => ""
}
}
///|
impl @moondb.Driver for SchemaLog with fn execute(
self : SchemaLog,
sql : String,
params : Array[@moondb.Value],
) -> @moondb.ExecResult raise @moondb.DbError {
if self.closed {
raise @moondb.Closed
}
if sql.has_prefix("INSERT INTO schema_migrations") {
self.applied.push((version_of(params[0]), text_of(params[1])))
} else if sql.has_prefix("DELETE FROM schema_migrations") {
let target = version_of(params[0])
for i = 0; i < self.applied.length(); i = i + 1 {
if self.applied[i].0 == target {
self.applied.remove(i) |> ignore
break
}
}
}
{ rows_affected: 1L, last_insert_id: 0L, }
}
///|
impl @moondb.Driver for SchemaLog with fn query(
self : SchemaLog,
sql : String,
params : Array[@moondb.Value],
) -> Array[@moondb.Row] raise @moondb.DbError {
if self.closed {
raise @moondb.Closed
}
let out : Array[@moondb.Row] = []
let row = fn(version : Int32) -> @moondb.Row {
{ columns: ["version"], values: [@moondb.Int(version)], }
}
if sql.has_prefix("SELECT version FROM schema_migrations WHERE version") {
let target = version_of(params[0])
for entry in self.applied {
if entry.0 == target {
out.push(row(entry.0))
}
}
} else if sql.has_prefix("SELECT version FROM schema_migrations") {
let versions = self.applied.map(fn(e) { e.0 })
versions.sort()
for v in versions {
out.push(row(v))
}
}
out
}
///|
impl @moondb.Driver for SchemaLog with fn begin(_self : SchemaLog) -> Unit raise @moondb.DbError {
}
///|
impl @moondb.Driver for SchemaLog with fn commit(_self : SchemaLog) -> Unit raise @moondb.DbError {
}
///|
impl @moondb.Driver for SchemaLog with fn rollback(_self : SchemaLog) -> Unit raise @moondb.DbError {
}
///|
impl @moondb.Driver for SchemaLog with fn close(self : SchemaLog) -> Unit {
self.closed = true
}
///|
#coverage.skip
fn main {
// Two migrations, handed in out of version order — the Migrator sorts them.
let v1 : @moonorm.Migration = {
version: 1,
name: "create_heroes",
up: ["CREATE TABLE heroes (id INTEGER PRIMARY KEY, name TEXT NOT NULL)"],
down: ["DROP TABLE heroes"],
}
let v2 : @moonorm.Migration = {
version: 2,
name: "create_teams",
up: [
"CREATE TABLE teams (id INTEGER PRIMARY KEY, name TEXT NOT NULL)", "CREATE INDEX idx_teams_name ON teams (name)",
],
down: ["DROP INDEX idx_teams_name", "DROP TABLE teams"],
}
let migrations = [v2, v1]
try {
let sess = @moonorm.Session::new(SchemaLog::{ applied: [], closed: false, })
let mig = @moonorm.Migrator::new()
println("current version: \{mig.current_version(sess)}")
let n = mig.up(sess, migrations)
println("applied \{n} migrations -> version \{mig.current_version(sess)}")
println(
"applied versions: " +
mig.applied_versions(sess).map(fn(v) { v.to_string() }).join(", "),
)
// Re-running up() is idempotent: nothing is pending.
println("second up() applied: \{mig.up(sess, migrations)}")
// Roll back everything above version 1.
let back = mig.down_to(sess, migrations, 1)
println(
"rolled back \{back} migrations -> version \{mig.current_version(sess)}",
)
println(
"applied versions: " +
mig.applied_versions(sess).map(fn(v) { v.to_string() }).join(", "),
)
sess.close()
} catch {
e => println("migration error: " + e.to_string())
}
}