use std::cell::RefCell;
use std::cell::RefMut;
use std::collections::HashMap;
use std::fs::create_dir_all;
use std::ops::Range;
use std::path::Path;
use ic_stable_structures::memory_manager::MemoryId;
use ic_stable_structures::{DefaultMemoryImpl, memory_manager::MemoryManager};
const FS_MEMORY_RANGE: Range<u8> = 101..119;
const DEFAULT_MOUNTED_DB_ID: u8 = 120;
const DEFAULT_DB_FILE_NAME: &str = "./DB/main.db";
pub use ic_wasi_polyfill;
pub use rusqlite;
pub use rusqlite::*;
thread_local! {
pub static MEMORY_MANAGER: RefCell<MemoryManager<DefaultMemoryImpl>> = {
let m = MemoryManager::init(DefaultMemoryImpl::default());
ic_wasi_polyfill::init_with_memory_manager(&[0u8; 32], &[("SQLITE_TMPDIR", "tmp")], &m, FS_MEMORY_RANGE);
let _ = std::fs::create_dir_all("tmp");
RefCell::new(m)
};
pub static CONNECTION: RefCell<Option<Connection>> = RefCell::new(Some(create_connection()));
pub static CONNECTION_SETUP: RefCell<ConnectionConfig> = RefCell::new(ConnectionConfig::default());
}
#[derive(Clone, Debug)]
pub struct ConnectionConfig {
pub db_file_name: String,
pub db_file_mount_id: Option<u8>,
pub pragma_settings: HashMap<String, String>,
}
impl ConnectionConfig {
pub fn new() -> Self {
ConnectionConfig::default()
}
}
impl Default for ConnectionConfig {
fn default() -> Self {
let mut default_pragmas = HashMap::new();
default_pragmas.insert("journal_mode".to_string(), "PERSIST".to_string());
default_pragmas.insert("synchronous".to_string(), "OFF".to_string());
default_pragmas.insert("locking_mode".to_string(), "EXCLUSIVE".to_string());
default_pragmas.insert("temp_store".to_string(), "MEMORY".to_string());
default_pragmas.insert("page_size".to_string(), "4096".to_string());
default_pragmas.insert("cache_size".to_string(), "500000".to_string());
Self {
db_file_name: DEFAULT_DB_FILE_NAME.to_string(),
db_file_mount_id: Some(DEFAULT_MOUNTED_DB_ID),
pragma_settings: default_pragmas,
}
}
}
pub fn close_connection() {
CONNECTION.with(|conn| {
let mut conn_mut = conn.borrow_mut();
if let Some(c) = conn_mut.take() {
drop(c);
}
})
}
pub fn get_connection_config() -> ConnectionConfig {
CONNECTION_SETUP.with(|setup| setup.borrow().clone())
}
pub fn set_connection_config(new_setup: ConnectionConfig) {
CONNECTION_SETUP.with(|setup| {
let mut s = setup.borrow_mut();
*s = new_setup
});
}
fn prepare_db_folder(db_file_name: &str) {
let path = Path::new(db_file_name);
let parent = path.parent();
if let Some(parent) = parent {
let res = create_dir_all(parent);
if let Err(er) = res {
println!("Error creating parent folder {er:?}");
}
}
let _ = std::fs::remove_dir_all(format!("{}.lock", db_file_name));
}
fn remount_db_file(db_file_name: &str, mount_id: Option<u8>) {
ic_wasi_polyfill::unmount_memory_file(db_file_name);
if let Some(mount_id) = mount_id {
let memory = MEMORY_MANAGER.with_borrow(|m| m.get(MemoryId::new(mount_id)));
let r = ic_wasi_polyfill::mount_memory_file(
db_file_name,
Box::new(memory),
ic_wasi_polyfill::MountedFileSizePolicy::MemoryPages,
);
if r > 0 {
println!("Error {r}: failed mounting database file {db_file_name}!");
}
}
}
fn create_connection() -> Connection {
MEMORY_MANAGER.with_borrow(|_m| {
});
let setup = get_connection_config();
prepare_db_folder(&setup.db_file_name);
remount_db_file(&setup.db_file_name, setup.db_file_mount_id);
let conn =
rusqlite::Connection::open(setup.db_file_name).expect("Failed opening the database!");
for (k, v) in &setup.pragma_settings {
conn.pragma_update(None, k, v as &dyn ToSql).unwrap();
}
conn
}
pub fn with_connection<F, R>(f: F) -> R
where
F: FnOnce(RefMut<'_, Connection>) -> R,
{
CONNECTION.with(|conn| {
let mut conn_mut: RefMut<'_, Option<_>> = conn.borrow_mut();
if conn_mut.is_none() {
*conn_mut = Some(create_connection());
}
let conn_ref = RefMut::map(conn_mut, |opt| opt.as_mut().unwrap());
f(conn_ref)
})
}