use crate::db::DbKind;
use once_cell::sync::Lazy;
use rusqlite::{Connection, Transaction};
pub static SCHEMA_CELL: Lazy<Schema> = Lazy::new(|| Schema {
migrations: vec![
M::initial(include_str!("sql/cell/schema/0.sql")),
M {
forward: include_str!("sql/cell/schema/1-up.sql").into(),
},
M {
forward: include_str!("sql/cell/schema/2-up.sql").into(),
},
M {
forward: include_str!("sql/cell/schema/3-up.sql").into(),
},
M {
forward: include_str!("sql/cell/schema/4-up.sql").into(),
},
M {
forward: include_str!("sql/cell/schema/5-up.sql").into(),
},
M {
forward: include_str!("sql/cell/schema/6-up.sql").into(),
},
M {
forward: include_str!("sql/cell/schema/7-up.sql").into(),
},
M {
forward: include_str!("sql/cell/schema/8-up.sql").into(),
},
M {
forward: include_str!("sql/cell/schema/9-up.sql").into(),
},
],
});
pub static SCHEMA_CONDUCTOR: Lazy<Schema> = Lazy::new(|| Schema {
migrations: vec![
M::initial(include_str!("sql/conductor/schema/0.sql")),
M {
forward: include_str!("sql/conductor/schema/1-up.sql").into(),
},
],
});
pub static SCHEMA_WASM: Lazy<Schema> = Lazy::new(|| Schema {
migrations: vec![
M::initial(include_str!("sql/wasm/schema/0.sql")),
M {
forward: include_str!("sql/wasm/schema/1-up.sql").into(),
},
],
});
pub static SCHEMA_PEER_META_STORE: Lazy<Schema> = Lazy::new(|| Schema {
migrations: vec![
M::initial(include_str!("sql/peer_meta_store/schema/0.sql")),
M {
forward: include_str!("sql/peer_meta_store/schema/1-up.sql").into(),
},
M {
forward: include_str!("sql/peer_meta_store/schema/2-up.sql").into(),
},
],
});
pub struct Schema {
migrations: Vec<Migration>,
}
impl Schema {
pub fn initialize(
&self,
conn: &mut Connection,
db_kind: Option<DbKind>,
) -> rusqlite::Result<()> {
let user_version: u16 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
let db_kind = db_kind
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| "<no name>".to_string());
let migrations_applied = user_version as usize;
let num_migrations = self.migrations.len();
match migrations_applied.cmp(&(num_migrations)) {
std::cmp::Ordering::Less => {
let mut txn = conn.transaction()?;
for v in migrations_applied..num_migrations {
self.migrations[v].run_forward(&mut txn)?;
txn.pragma_update(None, "user_version", v + 1)?;
}
txn.commit()?;
tracing::info!(
"database forward migrated: {} from {} to {}",
db_kind,
migrations_applied,
num_migrations - 1,
);
}
std::cmp::Ordering::Equal => {
tracing::debug!(
"database needed no migration or initialization, good to go: {}",
db_kind
);
}
std::cmp::Ordering::Greater => {
unimplemented!("backward migrations unimplemented");
}
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct Migration {
forward: Sql,
}
impl Migration {
pub fn initial(schema: &str) -> Self {
Self {
forward: schema.into(),
}
}
pub fn run_forward(&self, txn: &mut Transaction) -> rusqlite::Result<()> {
txn.execute_batch(&self.forward)?;
Ok(())
}
}
type M = Migration;
type Sql = String;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_migrations_initial() {
let schema = Schema {
migrations: vec![
M::initial("CREATE TABLE Numbers (num INTEGER);"),
M {
forward: "CREATE TABLE Names (name TEXT);".into(),
},
],
};
let mut conn = Connection::open_in_memory().unwrap();
schema.initialize(&mut conn, None).unwrap();
assert_eq!(
conn.execute("INSERT INTO Numbers (num) VALUES (1)", ())
.unwrap(),
1
);
assert_eq!(
conn.execute("INSERT INTO Names (name) VALUES ('Mike')", ())
.unwrap(),
1
);
}
#[test]
fn test_migrations_sequential() {
let mut schema = Schema {
migrations: vec![M::initial("CREATE TABLE Numbers (num INTEGER);")],
};
let mut conn = Connection::open_in_memory().unwrap();
schema.initialize(&mut conn, None).unwrap();
assert_eq!(
conn.execute("INSERT INTO Numbers (num) VALUES (1)", ())
.unwrap(),
1
);
assert!(conn
.execute("INSERT INTO Names (name) VALUES ('Mike')", ())
.is_err());
schema.migrations = vec![
M::initial("This bad SQL won't run, phew!"),
M {
forward: "CREATE TABLE Names (name TEXT);".into(),
},
];
schema.initialize(&mut conn, None).unwrap();
assert_eq!(
conn.execute("INSERT INTO Numbers (num) VALUES (1)", ())
.unwrap(),
1
);
assert_eq!(
conn.execute("INSERT INTO Names (name) VALUES ('Mike')", ())
.unwrap(),
1
);
}
}