mod m0001_initial_version;
mod m0002_config;
mod m0003_payment_history;
mod m0004_unregistered_board;
mod m0005_lightning_receive;
mod m0006_exit_rework;
mod m0007_vtxo_refresh_expiry_threshold;
mod m0008_fee_rate_implementation;
mod m0009_add_movement_kind;
mod m0010_remove_keychain;
mod m0011_exit_ancestor_info;
mod m0012_round;
mod m0013_round_sync;
mod m0014_drop_past_round_sync;
mod m0015_optional_round_seq;
mod m0016_config;
mod m0017_great_state_cleanup;
mod m0018_htlc_recv_cltv_delta;
mod m0019_round_state;
mod m0020_new_movements_api;
mod m0021_fix_lightning_movements;
mod m0022_unreleased;
mod m0023_mailbox;
mod m0024_server_pubkey;
mod m0025_fees;
mod m0026_pending_offboard;
mod m0027_split_destination;
use anyhow::Context;
use log::debug;
use rusqlite::{Connection, Transaction};
use m0001_initial_version::Migration0001;
use m0002_config::Migration0002;
use m0003_payment_history::Migration0003;
use m0004_unregistered_board::Migration0004;
use m0005_lightning_receive::Migration0005;
use m0006_exit_rework::Migration0006;
use m0007_vtxo_refresh_expiry_threshold::Migration0007;
use m0008_fee_rate_implementation::Migration0008;
use m0009_add_movement_kind::Migration0009;
use m0010_remove_keychain::Migration0010;
use m0011_exit_ancestor_info::Migration0011;
use m0012_round::Migration0012;
use m0013_round_sync::Migration0013;
use m0014_drop_past_round_sync::Migration0014;
use m0015_optional_round_seq::Migration0015;
use m0016_config::Migration0016;
use m0017_great_state_cleanup::Migration0017;
use m0018_htlc_recv_cltv_delta::Migration0018;
use m0019_round_state::Migration0019;
use m0020_new_movements_api::Migration0020;
use m0021_fix_lightning_movements::Migration0021;
use m0022_unreleased::Migration0022;
use m0023_mailbox::Migration0023;
use m0024_server_pubkey::Migration0024;
use m0025_fees::Migration0025;
use m0026_pending_offboard::Migration0026;
use m0027_split_destination::Migration0027;
pub struct MigrationContext {}
impl MigrationContext {
pub fn new() -> Self {
MigrationContext {}
}
pub fn do_all_migrations(&self, conn: &mut Connection) -> anyhow::Result<()> {
let tx = conn.transaction().context("Failed to start transcation")?;
self.init_migrations(&tx)?;
tx.commit().context("Failed to commit transaction")?;
self.try_migration(conn, &Migration0001{})?;
self.try_migration(conn, &Migration0002{})?;
self.try_migration(conn, &Migration0003{})?;
self.try_migration(conn, &Migration0004{})?;
self.try_migration(conn, &Migration0005{})?;
self.try_migration(conn, &Migration0006{})?;
self.try_migration(conn, &Migration0007{})?;
self.try_migration(conn, &Migration0008{})?;
self.try_migration(conn, &Migration0009{})?;
self.try_migration(conn, &Migration0010{})?;
self.try_migration(conn, &Migration0011{})?;
self.try_migration(conn, &Migration0012{})?;
self.try_migration(conn, &Migration0013{})?;
self.try_migration(conn, &Migration0014{})?;
self.try_migration(conn, &Migration0015{})?;
self.try_migration(conn, &Migration0016{})?;
self.try_migration(conn, &Migration0017{})?;
self.try_migration(conn, &Migration0018{})?;
self.try_migration(conn, &Migration0019{})?;
self.try_migration(conn, &Migration0020{})?;
self.try_migration(conn, &Migration0021{})?;
self.try_migration(conn, &Migration0022{})?;
self.try_migration(conn, &Migration0023{})?;
self.try_migration(conn, &Migration0024{})?;
self.try_migration(conn, &Migration0025{})?;
self.try_migration(conn, &Migration0026{})?;
self.try_migration(conn, &Migration0027{})?;
Ok(())
}
fn init_migrations(&self, conn: &Connection) -> anyhow::Result<i64> {
self.create_migrations_table_if_not_exists(conn)?;
match self.get_current_version(conn) {
Ok(version) => Ok(version),
Err(_) => {
self.update_version(conn, 0)?;
Ok(0)
}
}
}
fn try_migration(
&self,
conn: &mut Connection,
migration: &impl Migration
) -> anyhow::Result<()> {
let tx = conn.transaction().context("Failed to init transaction")?;
let current_version = self.get_current_version(&tx)?;
let from_version = migration.from_version();
if current_version == from_version {
debug!("Performing migration {}", migration.summary());
migration.do_migration(&tx)?;
self.update_version(&tx, migration.to_version())?;
} else if current_version < from_version {
bail!("Failed to perform migration. Database is at {} for migration {}",
current_version,
migration.summary()
);
}
tx.commit().context("Failed to commit transaction")?;
Ok(())
}
fn get_current_version(&self, conn: &Connection) -> anyhow::Result<i64> {
const ERR_MSG : &'static str = "Failed to get_current_version from database";
let query = "SELECT value FROM migrations ORDER BY value DESC LIMIT 1";
let mut statement = conn.prepare(query).context(ERR_MSG)?;
let mut rows = statement.query(()).context(ERR_MSG)?;
let row = rows.next().context(ERR_MSG)?
.context("the current schema version is not defined in the databases")?;
Ok(row.get(0).context(ERR_MSG)?)
}
fn update_version(&self, conn: &Connection, new_version: i64) -> anyhow::Result<i64> {
const ERR_MSG : &'static str = "Failed to update_version for database";
let query = "INSERT INTO migrations (value) VALUES (?1)";
let mut statement = conn.prepare(query).context(ERR_MSG)?;
statement.execute([new_version]).context(ERR_MSG)?;
Ok(new_version)
}
fn create_migrations_table_if_not_exists(&self, conn: &Connection) -> anyhow::Result<()> {
let query =
"CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')),
value INTEGER NOT NULL
)";
conn.execute(query, ()).context("Failed to create migration table")?;
Ok(())
}
}
trait Migration {
fn name(&self) -> &str;
fn to_version(&self) -> i64;
fn from_version(&self) -> i64 {
self.to_version() - 1
}
fn do_migration(&self, conn: &Transaction) -> anyhow::Result<()>;
fn summary(&self) -> String {
format!("{}->{}:'{}'", self.from_version(), self.to_version(), self.name())
}
}
#[cfg(test)]
mod test {
use super::*;
fn table_exists(conn: &Connection, table_name: &str) -> anyhow::Result<bool> {
let query = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?";
let mut statement = conn.prepare(query).context("Invalid query")?;
let mut rows = statement.query((table_name,)).context("Failed to execute query")?;
if let Some(_row) = rows.next().unwrap() {
return Ok(true)
} else {
return Ok(false)
}
}
fn assert_current_version(conn: &Connection, expected: i64) -> anyhow::Result<()> {
let ctx = MigrationContext::new();
let current_version = ctx.get_current_version(conn)?;
if current_version == expected {
Ok(())
} else {
bail!("Migration error; Expected version {} but database was at {}", expected, current_version);
}
}
#[test]
fn test_set_schema_version() {
let conn = rusqlite::Connection::open(":memory:").unwrap();
let migs = MigrationContext::new();
migs.init_migrations(&conn).unwrap();
assert_current_version(&conn, 0).unwrap();
migs.update_version(&conn, 1).unwrap();
assert_current_version(&conn, 1).unwrap();
}
#[test]
fn test_good_migration() {
let mut conn = rusqlite::Connection::open(":memory:").unwrap();
let migs = MigrationContext::new();
migs.init_migrations(&conn).unwrap();
assert_current_version(&conn, 0).unwrap();
migs.do_all_migrations(&mut conn).unwrap();
assert_current_version(&conn, 27).unwrap();
assert!(table_exists(&conn, "bark_vtxo").unwrap());
assert!(table_exists(&conn, "bark_vtxo_state").unwrap());
assert!(table_exists(&conn, "bark_movements").unwrap());
assert!(table_exists(&conn, "bark_movements_sent_to").unwrap());
assert!(table_exists(&conn, "bark_movements_received_on").unwrap());
assert!(table_exists(&conn, "bark_movements_input_vtxos").unwrap());
assert!(table_exists(&conn, "bark_movements_output_vtxos").unwrap());
assert!(table_exists(&conn, "bark_movements_exited_vtxos").unwrap());
assert!(table_exists(&conn, "bark_pending_lightning_receive").unwrap());
assert!(table_exists(&conn, "bark_exit_states").unwrap());
assert!(table_exists(&conn, "bark_exit_child_transactions").unwrap());
assert!(table_exists(&conn, "bark_round_state").unwrap());
assert!(table_exists(&conn, "bark_lightning_send").unwrap());
assert!(table_exists(&conn, "bark_mailbox_checkpoint").unwrap());
assert!(table_exists(&conn, "bark_pending_offboard").unwrap());
migs.do_all_migrations(&mut conn).unwrap();
}
struct BadMigration {}
impl Migration for BadMigration {
fn name(&self) -> &str { "Bad migration"}
fn to_version(&self) -> i64 { 1 }
fn do_migration(&self, tx: &Transaction) -> anyhow::Result<()> {
let good_query =
"CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)";
let bad_query = "NOT VALID SQL";
tx.execute(good_query, ())?;
tx.execute(bad_query, ())?;
Ok(())
}
}
#[test]
fn test_bad_migration() {
let mut conn = rusqlite::Connection::open_in_memory().unwrap();
let migs = MigrationContext::new();
migs.init_migrations(&conn).unwrap();
migs.try_migration(&mut conn, &BadMigration{})
.expect_err("The bad migration failed");
assert_current_version(&conn, 0).unwrap();
assert!(! table_exists(&conn, "test").unwrap());
}
}