use ic_sqlite_vfs::db::migrate::Migration;
use ic_sqlite_vfs::sqlite_vfs::{lock, stable_blob};
use ic_sqlite_vfs::stable::memory;
use ic_sqlite_vfs::stable::meta::Superblock;
use ic_sqlite_vfs::{params, Db};
use serial_test::serial;
use std::collections::BTreeMap;
fn reset() {
stable_blob::invalidate_read_cache();
memory::reset_for_tests();
lock::reset_for_tests();
Db::init(memory::memory_for_tests()).unwrap();
}
#[test]
#[serial]
fn failed_migration_does_not_advance_schema_version() {
reset();
let result = Db::migrate(&[
Migration {
version: 1,
sql: "CREATE TABLE ok_table(id INTEGER PRIMARY KEY);",
},
Migration {
version: 2,
sql: "CREATE TABLE broken(",
},
]);
assert!(result.is_err());
assert_eq!(Superblock::load().unwrap().schema_version, 0);
}
#[test]
#[serial]
fn duplicate_migration_version_is_rejected_before_schema_changes() {
reset();
let result = Db::migrate(&[
Migration {
version: 1,
sql: "CREATE TABLE duplicate_first(id INTEGER PRIMARY KEY);",
},
Migration {
version: 1,
sql: "CREATE TABLE duplicate_second(id INTEGER PRIMARY KEY);",
},
]);
assert!(result.is_err());
assert_eq!(Superblock::load().unwrap().schema_version, 0);
Db::migrate(&[Migration {
version: 2,
sql: "CREATE TABLE after_duplicate(id INTEGER PRIMARY KEY);",
}])
.unwrap();
let exists = Db::query(|connection| {
connection.query_scalar::<i64>(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'duplicate_first'",
params![],
)
})
.unwrap();
assert_eq!(exists, 0);
}
#[test]
#[serial]
fn deterministic_fuzz_matches_model() {
reset();
Db::migrate(&[Migration {
version: 1,
sql: "CREATE TABLE fuzz(k INTEGER PRIMARY KEY, v INTEGER NOT NULL);",
}])
.unwrap();
let mut model = BTreeMap::new();
let mut state = 7_u64;
for _ in 0..250 {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
let key = state % 32;
let value = state % 10_000;
if state & 1 == 0 {
model.insert(key, value);
Db::update(|connection| {
connection.execute_batch(&format!(
"INSERT INTO fuzz(k, v) VALUES ({key}, {value})
ON CONFLICT(k) DO UPDATE SET v = excluded.v"
))
})
.unwrap();
} else {
model.remove(&key);
Db::update(|connection| {
connection.execute_batch(&format!("DELETE FROM fuzz WHERE k = {key}"))
})
.unwrap();
}
}
let sum = Db::query(|connection| {
connection.query_scalar::<i64>("SELECT COALESCE(SUM(v), 0) FROM fuzz", params![])
})
.unwrap();
let expected = model.values().sum::<u64>();
assert_eq!(u64::try_from(sum).unwrap(), expected);
assert_eq!(Db::integrity_check().unwrap(), "ok");
}
#[test]
#[serial]
fn capacity_and_import_bounds_are_rejected() {
reset();
Db::migrate(&[Migration {
version: 1,
sql: "CREATE TABLE cap(id INTEGER PRIMARY KEY);",
}])
.unwrap();
let db_size = Superblock::load().unwrap().db_size;
let checksum = Db::refresh_checksum().unwrap();
Db::begin_import(db_size, checksum).unwrap();
let result = Db::import_chunk(db_size + 1, &[1, 2, 3]);
assert!(result.is_err());
}
#[test]
#[serial]
fn long_endurance_many_transactions_keeps_integrity() {
reset();
Db::migrate(&[Migration {
version: 1,
sql: "CREATE TABLE endurance(id INTEGER PRIMARY KEY, v INTEGER NOT NULL);",
}])
.unwrap();
for id in 0..1_000_u64 {
Db::update(|connection| {
connection.execute_batch(&format!("INSERT INTO endurance(id, v) VALUES ({id}, {id})"))
})
.unwrap();
}
let count = Db::query(|connection| {
connection.query_scalar::<i64>("SELECT COUNT(*) FROM endurance", params![])
})
.unwrap();
assert_eq!(count, 1_000);
assert_eq!(Db::integrity_check().unwrap(), "ok");
}