use std::path::PathBuf;
use logdive_core::{Indexer, LogdiveError, Result};
#[derive(Debug, Clone)]
pub struct AppState {
pub db_path: PathBuf,
}
impl AppState {
pub fn new(db_path: PathBuf) -> Self {
Self { db_path }
}
pub async fn with_connection<F, T>(&self, f: F) -> Result<T>
where
F: FnOnce(&Indexer) -> Result<T> + Send + 'static,
T: Send + 'static,
{
let path = self.db_path.clone();
let join_result = tokio::task::spawn_blocking(move || -> Result<T> {
let indexer = Indexer::open_read_only(&path)?;
f(&indexer)
})
.await;
match join_result {
Ok(inner) => inner,
Err(join_err) => {
let io_err = std::io::Error::other(format!("blocking task failed: {join_err}"));
Err(LogdiveError::io_at(&self.db_path, io_err))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn with_connection_runs_closure_and_propagates_result() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("ws.db");
let _ = Indexer::open(&db).expect("create db");
let state = AppState::new(db.clone());
let stats = state
.with_connection(|idx| idx.stats())
.await
.expect("with_connection");
assert_eq!(stats.entries, 0);
assert!(stats.tags.is_empty());
}
#[tokio::test]
async fn with_connection_errors_when_db_is_missing() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("missing.db");
let state = AppState::new(missing);
let err = state
.with_connection(|idx| idx.stats())
.await
.expect_err("should fail when db missing");
assert!(matches!(err, LogdiveError::Sqlite(_)));
}
#[tokio::test]
async fn with_connection_uses_read_only_connection() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("ro.db");
let _ = Indexer::open(&db).unwrap();
let state = AppState::new(db);
let result = state
.with_connection(|idx| {
idx.connection()
.execute(
"INSERT INTO log_entries (timestamp, raw, raw_hash) \
VALUES ('x', 'y', 'z')",
[],
)
.map_err(LogdiveError::from)
})
.await;
assert!(result.is_err(), "expected RO write rejection");
}
#[tokio::test]
async fn with_connection_surfaces_panic_as_io_error() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("panic.db");
let _ = Indexer::open(&db).unwrap();
let state = AppState::new(db);
let err = state
.with_connection(|_idx| -> Result<()> { panic!("intentional test panic") })
.await
.expect_err("panic should propagate as error, not silent success");
assert!(matches!(err, LogdiveError::Io { .. }));
}
}