use super::{SqlConn, SqlError, SqlValue, TxFn, count};
use crate::store::StoreError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Migration {
pub version: u32,
pub statements: &'static [&'static str],
}
pub const BOOTSTRAP: &str = "CREATE TABLE IF NOT EXISTS mkit_schema \
(id INTEGER PRIMARY KEY CHECK (id = 1), version INTEGER NOT NULL)";
const READ_VERSION: &str = "SELECT version FROM mkit_schema WHERE id = 1";
const WRITE_VERSION: &str = "INSERT INTO mkit_schema (id, version) VALUES (1, ?1) \
ON CONFLICT (id) DO UPDATE SET version = excluded.version";
pub const MIGRATIONS: &[Migration] = &[
Migration {
version: 1,
statements: &[
"CREATE TABLE IF NOT EXISTS kv (part BLOB NOT NULL, key BLOB NOT NULL, \
value BLOB NOT NULL, PRIMARY KEY (part, key)) WITHOUT ROWID",
],
},
Migration {
version: 2,
statements: &[
"CREATE INDEX IF NOT EXISTS kv_timers ON kv (key, part) WHERE key >= x'7700' AND key < x'7701'",
],
},
];
pub const SCHEMA_VERSION: u32 = 2;
fn stored_version<C: SqlConn>(conn: &C) -> Result<u32, SqlError> {
match conn.query(READ_VERSION, &[])?.first() {
None => Ok(0),
Some(row) => u32::try_from(count(row, 0)?).map_err(|_| SqlError::Corrupt("schema version")),
}
}
pub fn require_current<C: SqlConn>(conn: &C) -> Result<u32, StoreError> {
if conn
.query(
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'mkit_schema'",
&[],
)?
.is_empty()
{
return Err(StoreError::Unsupported(format!(
"database schema version 0 differs from binary version {SCHEMA_VERSION}; export with a matching binary"
).into()));
}
let version = stored_version(conn)?;
if version != SCHEMA_VERSION {
return Err(StoreError::Unsupported(format!(
"database schema version {version} differs from binary version {SCHEMA_VERSION}; export with a matching binary"
).into()));
}
Ok(version)
}
enum Step {
Applied,
Done(u32),
}
pub fn migrate<C: SqlConn>(conn: &C) -> Result<u32, StoreError> {
loop {
let step: TxFn<C, Step> = Box::new(|c: C| {
c.exec(BOOTSTRAP, &[])?;
let current = stored_version(&c)?;
let Some(next) = MIGRATIONS.iter().find(|m| m.version > current) else {
return Ok(Step::Done(current));
};
for statement in next.statements {
c.exec(statement, &[])?;
}
c.exec(WRITE_VERSION, &[SqlValue::Integer(next.version.into())])?;
Ok(Step::Applied)
});
match conn.transaction(step)? {
Step::Applied => {}
Step::Done(version) if version > SCHEMA_VERSION => {
return Err(StoreError::Unsupported(
"database schema is newer than this binary".into(),
));
}
Step::Done(version) => return Ok(version),
}
}
}