use std::{borrow::Cow, error::Error as StdError, fmt, path::Path, sync::Arc}; use futures::lock::{Mutex, MutexGuard}; use rusqlite::{Connection, Row, ToSql}; pub use rusqlite::{Error as RsqlError, params}; #[derive(Clone, Debug)] pub struct DatabaseHandle { connection: Arc>, } impl DatabaseHandle { pub fn new(connection: Connection) -> Self { Self { connection: Arc::new(Mutex::new(connection)), } } pub fn open(path: &Path) -> Result { Connection::open(path).map(|c| Self { connection: Arc::new(Mutex::new(c)), }) } pub async fn lock<'db>(&'db self) -> DatabaseLock<'db> { DatabaseLock { guard: self.connection.lock().await, } } } pub struct DatabaseLock<'db> { guard: MutexGuard<'db, Connection>, } impl<'db> DatabaseLock<'db> { pub fn insert>>( &self, sql: SQL, params: &[&dyn ToSql], ) -> Result<(), DatabaseError> { let query = sql.into(); let mut vec = vec![]; for parm in params.iter() { vec.push(parm as &dyn ToSql); } match self.guard.execute(query.as_ref(), vec.as_slice()) { Err(sqle) => Err(DatabaseError::sqlerror_with_context(sqle, "insert")), Ok(_) => Ok(()), } } pub fn query_one>, T, F>( &self, sql: SQL, params: &[&dyn ToSql], f: F, ) -> Result where F: Fn(&Row<'_>) -> Result, { let query = sql.into(); let mut stmt = self .guard .prepare(&query) .map_err(|err| DatabaseError::sqlerror_with_context(err, "prepare for select"))?; match stmt.query(params) { Ok(mut rows) => { let mut result: Option = None; while let Some(row) = rows .next() .map_err(|err| DatabaseError::sqlerror_with_context(err, "select"))? { if result.is_none() { result = Some(f(row).map_err(|e| e.with_context("select"))?); } else { return Err(DatabaseError::TooManyRows); } } match result { Some(t) => Ok(t), None => Err(DatabaseError::NoRows), } } Err(err) => Err(DatabaseError::sqlerror_with_context(err, "select")), } } pub fn create_table>>( &self, tablename: &'static str, sql: SQL, ) -> Result<(), DatabaseError> { let query = sql.into(); match self.guard.execute(query.as_ref(), rusqlite::params![]) { Err(err) => Err(DatabaseError::CreateTableFailed { table: tablename, err, }), Ok(_) => Ok(()), } } pub fn execute>>( &self, sql: SQL, params: &[&dyn ToSql], ) -> Result<(), DatabaseError> { let query = sql.into(); self.guard.execute(query.as_ref(), params)?; Ok(()) } } #[derive(Debug)] pub enum DatabaseError { CreateTableFailed { table: &'static str, err: RsqlError, }, SqliteError { context: Option<&'static str>, err: RsqlError, }, NoRows, TooManyRows, } impl StdError for DatabaseError {} impl DatabaseError { pub fn sqlerror_with_context(sqle: RsqlError, context: &'static str) -> Self { Self::SqliteError { context: Some(context), err: sqle, } } /// Adds context to an SqliteError, or does nothing if it's another variant pub fn with_context(self, context: &'static str) -> Self { if let Self::SqliteError { context: None, err } = self { DatabaseError::SqliteError { context: Some(context), err, } } else { self } } } impl fmt::Display for DatabaseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::CreateTableFailed { table, err } => { write!(f, "failed to create table {table}: {err}") } Self::SqliteError { context, err } => match context { None => write!(f, "rusqlite error: {err}"), Some(ctx) => write!(f, "rusqlite error ({ctx}): {err}"), }, Self::NoRows => write!(f, "query returned no rows"), Self::TooManyRows => write!(f, "query returned too many rows"), } } } impl From for DatabaseError { fn from(sqle: RsqlError) -> Self { DatabaseError::SqliteError { context: None, err: sqle, } } }