use std::path::Path;
use std::time::Duration;
use turso::{Builder, Connection};
use crate::{Error, Result, env};
pub const DEFAULT_PATH: &str = "database/database.db";
pub const BUSY_TIMEOUT: Duration = Duration::from_secs(5);
pub fn path_from_env() -> String {
env::var_or("DATABASE_PATH", DEFAULT_PATH)
}
#[derive(Clone, Debug)]
pub struct Database {
inner: turso::Database,
}
impl Database {
pub async fn open(path: &str) -> Result<Self> {
if path != ":memory:" {
if let Some(dir) = Path::new(path)
.parent()
.filter(|d| !d.as_os_str().is_empty())
{
std::fs::create_dir_all(dir).map_err(|source| Error::DatabaseDirectory {
path: dir.to_path_buf(),
source,
})?;
}
}
let inner = Builder::new_local(path).build().await?;
Ok(Self { inner })
}
pub async fn connect(&self) -> Result<Connection, turso::Error> {
let conn = self.inner.connect()?;
conn.busy_timeout(BUSY_TIMEOUT)?;
conn.execute("PRAGMA foreign_keys = ON", ()).await?;
Ok(conn)
}
}
pub async fn connect(path: &str) -> Result<Connection> {
Ok(Database::open(path).await?.connect().await?)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn concurrent_writers_wait_instead_of_failing() {
let dir = std::env::temp_dir().join(format!("slave-db-{}", std::process::id()));
let path = dir.join("concurrent.db");
let db = Database::open(path.to_str().unwrap()).await.unwrap();
let conn = db.connect().await.unwrap();
conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER)", ())
.await
.unwrap();
let tasks: Vec<_> = (0..20)
.map(|n| {
let db = db.clone();
tokio::spawn(async move {
let conn = db.connect().await?;
conn.execute("INSERT INTO t (n) VALUES (?1)", (n,)).await
})
})
.collect();
for task in tasks {
task.await.unwrap().unwrap();
}
let mut rows = conn.query("SELECT count(*) FROM t", ()).await.unwrap();
let count: i64 = rows.next().await.unwrap().unwrap().get(0).unwrap();
assert_eq!(count, 20);
let _ = std::fs::remove_dir_all(dir);
}
#[tokio::test]
async fn foreign_keys_are_enforced() {
let conn = connect(":memory:").await.unwrap();
conn.execute("CREATE TABLE a (id INTEGER PRIMARY KEY)", ())
.await
.unwrap();
conn.execute(
"CREATE TABLE b (a_id INTEGER NOT NULL REFERENCES a(id))",
(),
)
.await
.unwrap();
let err = conn.execute("INSERT INTO b (a_id) VALUES (1)", ()).await;
assert!(err.is_err(), "insert with a dangling reference must fail");
}
}