use rusqlite::{Error, Transaction};
use super::Migration;
pub struct M004;
impl Migration for M004 {
fn id(&self) -> u32 {
4
}
fn name(&self) -> &'static str {
"raft_log_storage"
}
fn up(&self, tx: &Transaction<'_>) -> Result<(), Error> {
tx.execute_batch(
r#"
CREATE TABLE IF NOT EXISTS raft_log_entries (
log_index INTEGER PRIMARY KEY,
term INTEGER NOT NULL,
leader_node_id INTEGER NOT NULL,
payload BLOB NOT NULL
) STRICT;
-- Singleton vote row. CHECK constraint guarantees only one row.
CREATE TABLE IF NOT EXISTS raft_vote (
id INTEGER PRIMARY KEY CHECK (id = 1),
payload BLOB NOT NULL,
updated_at_unix_micros INTEGER NOT NULL
) STRICT;
-- Key/value pointers (last_purged_log_id, committed_log_id).
CREATE TABLE IF NOT EXISTS raft_state (
key TEXT PRIMARY KEY,
value BLOB NOT NULL,
updated_at_unix_micros INTEGER NOT NULL
) STRICT;
"#,
)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use rusqlite::Connection;
#[test]
fn migration_creates_three_tables() {
let mut conn = Connection::open_in_memory().unwrap();
let tx = conn.transaction().unwrap();
M004.up(&tx).unwrap();
tx.commit().unwrap();
let count: u32 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' \
AND name IN ('raft_log_entries', 'raft_vote', 'raft_state')",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(count, 3);
}
#[test]
fn migration_is_idempotent() {
let mut conn = Connection::open_in_memory().unwrap();
for _ in 0..3 {
let tx = conn.transaction().unwrap();
M004.up(&tx).unwrap();
tx.commit().unwrap();
}
let count: u32 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='raft_log_entries'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(count, 1);
}
#[test]
fn vote_table_singleton_check_rejects_second_row() {
let mut conn = Connection::open_in_memory().unwrap();
let tx = conn.transaction().unwrap();
M004.up(&tx).unwrap();
tx.commit().unwrap();
conn.execute(
"INSERT INTO raft_vote (id, payload, updated_at_unix_micros) VALUES (1, X'00', 0)",
[],
)
.unwrap();
let r = conn.execute(
"INSERT INTO raft_vote (id, payload, updated_at_unix_micros) VALUES (2, X'00', 0)",
[],
);
assert!(r.is_err(), "vote table CHECK must reject id != 1");
}
#[test]
fn strict_table_rejects_wrong_types_in_log() {
let mut conn = Connection::open_in_memory().unwrap();
let tx = conn.transaction().unwrap();
M004.up(&tx).unwrap();
tx.commit().unwrap();
let r = conn.execute(
"INSERT INTO raft_log_entries (log_index, term, leader_node_id, payload) \
VALUES (1, 'not_int', 0, X'00')",
[],
);
assert!(r.is_err(), "STRICT must reject string in INTEGER column");
}
}