use std::path::Path;
use flume::{self, Receiver, Sender};
use futures::channel::oneshot;
use rusqlite::{Connection, OptionalExtension, Params, Row};
type Task = Box<dyn FnOnce(&mut Connection) + Send + 'static>;
const CAPACITY: usize = 10;
#[derive(Debug, Clone)]
pub struct StorageHandle {
tx: Sender<Task>,
}
impl StorageHandle {
pub async fn open(path: impl AsRef<Path>) -> rusqlite::Result<Self> {
let tx = setup_database(path).await?;
Ok(Self { tx })
}
pub async fn open_in_memory() -> rusqlite::Result<Self> {
Self::open(":memory:").await
}
pub async fn execute<P>(
&self,
sql: impl AsRef<str>,
params: P,
) -> rusqlite::Result<usize>
where
P: Params + Clone + Send + 'static,
{
let sql = sql.as_ref().to_string();
self.submit_task(move |conn| {
let mut prepared = conn.prepare_cached(&sql)?;
prepared.execute(params)
})
.await
}
pub async fn execute_many<P>(
&self,
sql: impl AsRef<str>,
param_set: Vec<P>,
) -> rusqlite::Result<usize>
where
P: Params + Clone + Send + 'static,
{
let sql = sql.as_ref().to_string();
self.submit_task(move |conn| {
let tx = conn.transaction()?;
let mut total = 0;
{
let mut prepared = tx.prepare_cached(&sql)?;
for params in param_set {
total += prepared.execute(params)?;
}
}
tx.commit()?;
Ok(total)
})
.await
}
pub async fn fetch_one<P, T>(
&self,
sql: impl AsRef<str>,
params: P,
) -> rusqlite::Result<Option<T>>
where
P: Params + Send + 'static,
T: FromRow + Send + 'static,
{
let sql = sql.as_ref().to_string();
self.submit_task(move |conn| {
let mut prepared = conn.prepare_cached(&sql)?;
prepared.query_row(params, T::from_row).optional()
})
.await
}
pub async fn fetch_many<P, T>(
&self,
sql: impl AsRef<str>,
param_sets: Vec<P>,
) -> rusqlite::Result<Vec<T>>
where
P: Params + Send + 'static,
T: FromRow + Send + 'static,
{
let sql = sql.as_ref().to_string();
self.submit_task(move |conn| {
let mut prepared = conn.prepare_cached(&sql)?;
let mut rows = Vec::with_capacity(param_sets.len());
for params in param_sets {
if let Some(row) = prepared.query_row(params, T::from_row).optional()? {
rows.push(row);
}
}
Ok(rows)
})
.await
}
pub async fn fetch_all<P, T>(
&self,
sql: impl AsRef<str>,
params: P,
) -> rusqlite::Result<Vec<T>>
where
P: Params + Send + 'static,
T: FromRow + Send + 'static,
{
let sql = sql.as_ref().to_string();
self.submit_task(move |conn| {
let mut prepared = conn.prepare_cached(&sql)?;
let mut iter = prepared.query(params)?;
let mut rows = Vec::with_capacity(4);
while let Some(row) = iter.next()? {
rows.push(T::from_row(row)?);
}
Ok(rows)
})
.await
}
async fn submit_task<CB, T>(&self, inner: CB) -> rusqlite::Result<T>
where
T: Send + 'static,
CB: FnOnce(&mut Connection) -> rusqlite::Result<T> + Send + 'static,
{
let (tx, rx) = oneshot::channel();
let cb = move |conn: &mut Connection| {
let res = inner(conn);
let _ = tx.send(res);
};
self.tx
.send_async(Box::new(cb))
.await
.expect("send message");
rx.await.unwrap()
}
}
pub trait FromRow: Sized {
fn from_row(row: &Row) -> rusqlite::Result<Self>;
}
async fn setup_database(path: impl AsRef<Path>) -> rusqlite::Result<Sender<Task>> {
let path = path.as_ref().to_path_buf();
let (tx, rx) = flume::bounded(CAPACITY);
tokio::task::spawn_blocking(move || setup_disk_handle(&path, rx))
.await
.expect("spawn background runner")?;
Ok(tx)
}
fn setup_disk_handle(path: &Path, tasks: Receiver<Task>) -> rusqlite::Result<()> {
let disk = Connection::open(path)?;
disk.query_row("pragma journal_mode = WAL;", (), |_r| Ok(()))?;
disk.execute("pragma synchronous = normal;", ())?;
disk.execute("pragma temp_store = memory;", ())?;
std::thread::spawn(move || run_tasks(disk, tasks));
Ok(())
}
fn run_tasks(mut conn: Connection, tasks: Receiver<Task>) {
while let Ok(task) = tasks.recv() {
(task)(&mut conn);
}
}
#[cfg(test)]
mod tests {
use std::env::temp_dir;
use super::*;
#[tokio::test]
async fn test_memory_storage_handle() {
let handle = StorageHandle::open_in_memory().await.expect("open DB");
run_storage_handle_suite(handle).await;
}
#[tokio::test]
async fn test_disk_storage_handle() {
let path = temp_dir().join(uuid::Uuid::new_v4().to_string());
let handle = StorageHandle::open(path).await.expect("open DB");
run_storage_handle_suite(handle).await;
}
#[derive(Debug, Eq, PartialEq)]
struct Person {
id: i32,
name: String,
data: String,
}
impl FromRow for Person {
fn from_row(row: &Row) -> rusqlite::Result<Self> {
Ok(Self {
id: row.get(0)?,
name: row.get(1)?,
data: row.get(2)?,
})
}
}
async fn run_storage_handle_suite(handle: StorageHandle) {
handle
.execute(
"CREATE TABLE person (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
data BLOB
)",
(), )
.await
.expect("create table");
let res = handle
.fetch_one::<_, Person>("SELECT id, name, data FROM person;", ())
.await
.expect("execute statement");
assert!(res.is_none(), "Expected no rows to be returned.");
handle
.execute(
"INSERT INTO person (id, name, data) VALUES (1, 'cf8', 'tada');",
(),
)
.await
.expect("Insert row");
let res = handle
.fetch_one::<_, Person>("SELECT id, name, data FROM person;", ())
.await
.expect("execute statement");
assert_eq!(
res,
Some(Person {
id: 1,
name: "cf8".to_string(),
data: "tada".to_string()
}),
);
handle
.execute(
"INSERT INTO person (id, name, data) VALUES (2, 'cf6', 'tada2');",
(),
)
.await
.expect("Insert row");
let res = handle
.fetch_all::<_, Person>(
"SELECT id, name, data FROM person ORDER BY id ASC;",
(),
)
.await
.expect("execute statement");
assert_eq!(
res,
vec![
Person {
id: 1,
name: "cf8".to_string(),
data: "tada".to_string()
},
Person {
id: 2,
name: "cf6".to_string(),
data: "tada2".to_string()
},
],
);
}
}