use crate::persist::global::config::DatabaseConfig;
use crate::prelude::*;
use r2d2::{CustomizeConnection, Pool};
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::OpenFlags;
pub type ConnectionPool = Pool<SqliteConnectionManager>;
#[derive(Debug)]
struct TransactionCleanupCustomizer;
impl CustomizeConnection<rusqlite::Connection, rusqlite::Error> for TransactionCleanupCustomizer {
fn on_acquire(
&self,
conn: &mut rusqlite::Connection,
) -> std::result::Result<(), rusqlite::Error> {
let _ = conn.execute("ROLLBACK", []);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct DatabaseService {
pool: ConnectionPool,
}
static IN_MEMORY_DATABASES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
fn prepare_file(path: &Path, config: &DatabaseConfig) -> rusqlite::Result<()> {
if !config.db_enable_wal {
return Ok(());
}
let conn = rusqlite::Connection::open(path)?;
conn.pragma_update(None, "busy_timeout", config.db_busy_timeout_ms)?;
conn.pragma_update(None, "journal_mode", "WAL")?;
Ok(())
}
fn configure(conn: &rusqlite::Connection, config: &DatabaseConfig) -> rusqlite::Result<()> {
conn.pragma_update(None, "busy_timeout", config.db_busy_timeout_ms)?;
if config.db_enable_wal {
conn.pragma_update(None, "journal_mode", "WAL")?;
}
if config.db_enable_foreign_keys {
conn.pragma_update(None, "foreign_keys", "ON")?;
}
conn.pragma_update(None, "synchronous", "NORMAL")?;
conn.pragma_update(None, "cache_size", 10000)?;
conn.pragma_update(None, "temp_store", "memory")?;
conn.pragma_update(None, "mmap_size", 134217728)?;
conn.pragma_update(None, "wal_autocheckpoint", 1000)?;
Ok(())
}
impl DatabaseService {
pub fn new(config: &DatabaseConfig) -> anyhow::Result<Self> {
if config.db_path == ":memory:" {
info!("Database: Using in-memory database");
return Self::in_memory(Some(config.db_max_connections));
}
if let Some(parent) = Path::new(&config.db_path).parent() {
std::fs::create_dir_all(parent)
.map_err(|e| anyhow::anyhow!("Failed to create directory: {}", e))?;
}
prepare_file(Path::new(&config.db_path), config)
.map_err(|e| anyhow::anyhow!("Failed to prepare {}: {}", config.db_path, e))?;
let config_clone = config.clone();
let manager = SqliteConnectionManager::file(&config.db_path)
.with_flags(
OpenFlags::SQLITE_OPEN_READ_WRITE
| OpenFlags::SQLITE_OPEN_CREATE
| OpenFlags::SQLITE_OPEN_FULL_MUTEX,
)
.with_init(move |conn| configure(conn, &config_clone));
let pool = Pool::builder()
.max_size(config.db_max_connections)
.connection_timeout(std::time::Duration::from_secs(
config.db_connection_timeout_seconds,
))
.connection_customizer(Box::new(TransactionCleanupCustomizer))
.build(manager)
.map_err(|e| anyhow::anyhow!("Failed to create database pool: {}", e))?;
let service = Self { pool };
service.run_migrations()?;
Ok(service)
}
pub fn new_in_memory() -> anyhow::Result<Self> {
Self::in_memory(None)
}
pub fn new_in_memory_with_config(max_connections: Option<u32>) -> anyhow::Result<Self> {
Self::in_memory(max_connections)
}
fn in_memory(max_connections: Option<u32>) -> anyhow::Result<Self> {
let pool_size = max_connections.unwrap_or(1);
let name = format!(
"memdb-{}-{}",
std::process::id(),
IN_MEMORY_DATABASES.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
);
let manager = SqliteConnectionManager::file(format!("file:{name}?mode=memory&cache=shared"))
.with_flags(
OpenFlags::SQLITE_OPEN_READ_WRITE
| OpenFlags::SQLITE_OPEN_CREATE
| OpenFlags::SQLITE_OPEN_URI
| OpenFlags::SQLITE_OPEN_FULL_MUTEX,
)
.with_init(|conn| {
conn.pragma_update(None, "foreign_keys", "ON")?;
conn.pragma_update(None, "busy_timeout", 30000)?;
conn.pragma_update(None, "synchronous", "NORMAL")?;
conn.pragma_update(None, "cache_size", 10000)?;
conn.pragma_update(None, "temp_store", "memory")?;
Ok(())
});
let pool = Pool::builder()
.max_size(pool_size)
.connection_timeout(std::time::Duration::from_secs(5))
.connection_customizer(Box::new(TransactionCleanupCustomizer))
.build(manager)
.map_err(|e| anyhow::anyhow!("Failed to create in-memory database pool: {}", e))?;
let service = Self { pool };
service.run_migrations()?;
Ok(service)
}
pub fn get_connection(
&self,
) -> anyhow::Result<r2d2::PooledConnection<SqliteConnectionManager>> {
self.pool.get().map_err(Into::into)
}
pub fn run_migrations(&self) -> anyhow::Result<()> {
let conn = self.get_connection()?;
crate::persist::global::migrations::run_migrations(conn)?;
tracing::debug!("Database migrations completed successfully");
Ok(())
}
pub fn health_check(&self) -> anyhow::Result<()> {
let conn = self.get_connection()?;
let _: i32 = conn.query_row("SELECT 1", [], |row| row.get(0))?;
Ok(())
}
}
#[cfg(test)]
mod in_memory_tests {
use super::DatabaseService;
#[test]
fn test11513_two_in_memory_databases_do_not_share_a_row() {
let one = DatabaseService::new_in_memory().expect("the first database");
let two = DatabaseService::new_in_memory().expect("the second database");
let write = one.get_connection().expect("a connection to the first");
write
.execute("CREATE TABLE only_in_one (id INTEGER PRIMARY KEY)", [])
.expect("the first database takes a table of its own");
write
.execute("INSERT INTO only_in_one (id) VALUES (1)", [])
.expect("and a row in it");
let read = two.get_connection().expect("a connection to the second");
let found: Result<i64, _> = read.query_row("SELECT id FROM only_in_one", [], |row| row.get(0));
assert!(
found.is_err(),
"the second database can see the first one's table, so they are one \
database and every test that asked for its own got everybody's"
);
}
#[test]
fn test11514_one_in_memory_database_is_shared_across_its_connections() {
let service =
DatabaseService::new_in_memory_with_config(Some(4)).expect("a database with a pool");
let write = service.get_connection().expect("one connection");
write
.execute("CREATE TABLE shared_rows (id INTEGER PRIMARY KEY)", [])
.expect("a table");
write
.execute("INSERT INTO shared_rows (id) VALUES (7)", [])
.expect("a row");
let read = service.get_connection().expect("a second, concurrent connection");
let found: i64 = read
.query_row("SELECT id FROM shared_rows", [], |row| row.get(0))
.expect("the same database, seen from another connection");
assert_eq!(found, 7);
}
}
#[cfg(test)]
mod configure_tests {
use super::configure;
use crate::persist::global::config::DatabaseConfig;
#[test]
fn test12140_connections_configuring_a_new_database_together_all_succeed() {
for attempt in 0..5 {
let dir = tempfile::tempdir().expect("a directory");
let path = dir.path().join("fresh.db");
let config = DatabaseConfig::new(&path);
super::prepare_file(&path, &config).expect("the file is prepared");
let start = std::sync::Arc::new(std::sync::Barrier::new(8));
let handles: Vec<_> = (0..8)
.map(|_| {
let (path, config, start) = (path.clone(), config.clone(), start.clone());
std::thread::spawn(move || {
let conn = rusqlite::Connection::open(&path).expect("the file opens");
start.wait();
configure(&conn, &config).map_err(|e| e.to_string())
})
})
.collect();
for handle in handles {
let outcome = handle.join().expect("the thread finishes");
assert!(
outcome.is_ok(),
"attempt {attempt}: a connection failed to configure a new database: {outcome:?}"
);
}
}
}
}