mod error;
mod runtime;
mod stmt_lock;
mod value;
use std::mem::ManuallyDrop;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use runtime::{new_runtime, SharedRt};
use stmt_lock::StmtLock;
pub use error::{Error, Result};
pub use value::Value;
fn check_pid(opened: u32) -> Result<()> {
let current = std::process::id();
if current == opened {
Ok(())
} else {
Err(Error::Forked { opened, current })
}
}
#[derive(Clone)]
pub struct OpenOptions {
pub local_path: String,
pub remote_url: Option<String>,
pub auth_token: Option<String>,
pub bootstrap_if_empty: bool,
}
impl std::fmt::Debug for OpenOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OpenOptions")
.field("local_path", &self.local_path)
.field("remote_url", &self.remote_url.as_ref().map(|_| "[set]"))
.field(
"auth_token",
&self.auth_token.as_ref().map(|_| "[redacted]"),
)
.field("bootstrap_if_empty", &self.bootstrap_if_empty)
.finish()
}
}
impl Default for OpenOptions {
fn default() -> Self {
Self {
local_path: ":memory:".to_string(),
remote_url: None,
auth_token: None,
bootstrap_if_empty: true,
}
}
}
pub struct Row {
pub values: Vec<Value>,
}
pub struct QueryResult {
pub columns: Vec<String>,
pub decl_types: Vec<Option<String>>,
pub rows: Vec<Row>,
}
enum Inner {
Local(turso::Database),
Synced(turso::sync::Database),
}
pub struct Database {
rt: ManuallyDrop<SharedRt>,
pid: u32,
inner: Mutex<Option<Arc<Inner>>>,
sync_lock: Mutex<()>,
stmt_lock: Arc<StmtLock>,
}
pub struct Connection {
rt: ManuallyDrop<SharedRt>,
pid: u32,
stmt_lock: Arc<StmtLock>,
conn: Mutex<Option<turso::Connection>>,
}
impl Database {
pub fn open(opts: OpenOptions) -> Result<Database> {
let rt = new_runtime()?;
let inner = rt.block_on(async {
match (&opts.remote_url, &opts.auth_token) {
(Some(url), Some(token)) => {
let db = turso::sync::Builder::new_remote(&opts.local_path)
.with_remote_url(url)
.with_auth_token(token)
.bootstrap_if_empty(opts.bootstrap_if_empty)
.build()
.await
.map_err(|e| Error::from_engine(Error::Open, e))?;
Ok::<Inner, Error>(Inner::Synced(db))
}
_ => {
let db = turso::Builder::new_local(&opts.local_path)
.build()
.await
.map_err(|e| Error::from_engine(Error::Open, e))?;
Ok(Inner::Local(db))
}
}
})?;
Ok(Database {
rt: ManuallyDrop::new(rt),
pid: std::process::id(),
inner: Mutex::new(Some(Arc::new(inner))),
sync_lock: Mutex::new(()),
stmt_lock: stmt_lock::for_path(&opts.local_path),
})
}
fn handle(&self) -> Result<Arc<Inner>> {
check_pid(self.pid)?;
let guard = self.inner.lock().unwrap_or_else(|p| p.into_inner());
guard.as_ref().cloned().ok_or(Error::Closed)
}
pub fn connect(&self) -> Result<Connection> {
let inner = self.handle()?;
let conn = match &*inner {
Inner::Local(db) => db
.connect()
.map_err(|e| Error::from_engine(Error::Connect, e))?,
Inner::Synced(db) => self
.rt
.block_on(async { db.connect().await })
.map_err(|e| Error::from_engine(Error::Connect, e))?,
};
Ok(Connection {
rt: ManuallyDrop::new((*self.rt).clone()),
pid: self.pid,
stmt_lock: Arc::clone(&self.stmt_lock),
conn: Mutex::new(Some(conn)),
})
}
pub fn push(&self) -> Result<()> {
let inner = self.handle()?;
match &*inner {
Inner::Local(_) => Err(Error::NotSynced("push")),
Inner::Synced(db) => {
let _sync = self.sync_lock.lock().unwrap_or_else(|p| p.into_inner());
self.rt
.block_on(async { db.push().await })
.map(|_| ())
.map_err(|e| Error::from_engine(Error::Sync, e))
}
}
}
pub fn pull(&self) -> Result<bool> {
let inner = self.handle()?;
match &*inner {
Inner::Local(_) => Err(Error::NotSynced("pull")),
Inner::Synced(db) => {
let _sync = self.sync_lock.lock().unwrap_or_else(|p| p.into_inner());
self.rt
.block_on(async { db.pull().await })
.map_err(|e| Error::from_engine(Error::Sync, e))
}
}
}
pub fn close(&self) {
if check_pid(self.pid).is_err() {
match self.inner.try_lock() {
Ok(mut g) => std::mem::forget(g.take()),
Err(std::sync::TryLockError::Poisoned(p)) => {
std::mem::forget(p.into_inner().take())
}
Err(std::sync::TryLockError::WouldBlock) => {}
}
return;
}
let inner = self.inner.lock().unwrap_or_else(|p| p.into_inner()).take();
if inner.is_some() {
self.rt.block_on(async move { drop(inner) });
}
}
pub fn is_closed(&self) -> bool {
if check_pid(self.pid).is_err() {
return match self.inner.try_lock() {
Ok(g) => g.is_none(),
Err(std::sync::TryLockError::Poisoned(p)) => p.into_inner().is_none(),
Err(std::sync::TryLockError::WouldBlock) => true,
};
}
self.inner
.lock()
.unwrap_or_else(|p| p.into_inner())
.is_none()
}
}
impl Drop for Database {
fn drop(&mut self) {
if check_pid(self.pid).is_err() {
let slot = self.inner.get_mut().unwrap_or_else(|p| p.into_inner());
std::mem::forget(slot.take());
return;
}
self.close();
unsafe { ManuallyDrop::drop(&mut self.rt) };
}
}
impl Connection {
fn statement<T>(
&self,
interrupt: Option<&AtomicBool>,
run: impl FnOnce(&turso::Connection) -> Result<T>,
) -> Result<T> {
check_pid(self.pid)?;
let _stmt = self.stmt_lock.acquire(interrupt)?;
let guard = self.conn.lock().unwrap_or_else(|p| p.into_inner());
let conn = guard.as_ref().ok_or(Error::Closed)?;
conn.busy_timeout(Duration::ZERO)
.map_err(|e| Error::from_engine(Error::Query, e))?;
run(conn)
}
pub fn execute(&self, sql: &str, params: &[Value]) -> Result<u64> {
self.execute_inner(sql, params, None)
}
pub fn execute_interruptible(
&self,
sql: &str,
params: &[Value],
interrupt: &AtomicBool,
) -> Result<u64> {
self.execute_inner(sql, params, Some(interrupt))
}
fn execute_inner(
&self,
sql: &str,
params: &[Value],
interrupt: Option<&AtomicBool>,
) -> Result<u64> {
self.statement(interrupt, |conn| {
let tparams: Vec<turso::Value> = params.iter().map(Value::to_turso).collect();
self.rt.block_on(async {
conn.execute(sql, tparams)
.await
.map_err(|e| Error::from_engine(Error::Query, e))
})
})
}
pub fn query(&self, sql: &str, params: &[Value]) -> Result<Vec<Row>> {
Ok(self.query_result(sql, params)?.rows)
}
pub fn query_result(&self, sql: &str, params: &[Value]) -> Result<QueryResult> {
self.query_result_inner(sql, params, None)
}
pub fn query_result_interruptible(
&self,
sql: &str,
params: &[Value],
interrupt: &AtomicBool,
) -> Result<QueryResult> {
self.query_result_inner(sql, params, Some(interrupt))
}
fn query_result_inner(
&self,
sql: &str,
params: &[Value],
interrupt: Option<&AtomicBool>,
) -> Result<QueryResult> {
self.statement(interrupt, |conn| {
let tparams: Vec<turso::Value> = params.iter().map(Value::to_turso).collect();
self.rt.block_on(async {
let mut rows = conn
.query(sql, tparams)
.await
.map_err(|e| Error::from_engine(Error::Query, e))?;
let cols = rows.columns();
let columns = cols.iter().map(|c| c.name().to_string()).collect();
let decl_types = cols
.iter()
.map(|c| c.decl_type().map(str::to_string))
.collect();
let mut out = Vec::new();
while let Some(row) = rows
.next()
.await
.map_err(|e| Error::from_engine(Error::Query, e))?
{
let n = row.column_count();
let mut vals = Vec::with_capacity(n);
for i in 0..n {
let cv = row
.get_value(i)
.map_err(|e| Error::from_engine(Error::Query, e))?;
vals.push(Value::from_turso(cv));
}
out.push(Row { values: vals });
}
Ok(QueryResult {
columns,
decl_types,
rows: out,
})
})
})
}
pub fn last_insert_rowid(&self) -> Result<i64> {
check_pid(self.pid)?;
let guard = self.conn.lock().unwrap_or_else(|p| p.into_inner());
let conn = guard.as_ref().ok_or(Error::Closed)?;
Ok(conn.last_insert_rowid())
}
pub fn execute_batch(&self, sql: &str) -> Result<()> {
self.execute_batch_inner(sql, None)
}
pub fn execute_batch_interruptible(&self, sql: &str, interrupt: &AtomicBool) -> Result<()> {
self.execute_batch_inner(sql, Some(interrupt))
}
fn execute_batch_inner(&self, sql: &str, interrupt: Option<&AtomicBool>) -> Result<()> {
self.statement(interrupt, |conn| {
self.rt.block_on(async {
conn.execute_batch(sql)
.await
.map_err(|e| Error::from_engine(Error::Query, e))
})
})
}
pub fn close(&self) {
if check_pid(self.pid).is_err() {
match self.conn.try_lock() {
Ok(mut g) => std::mem::forget(g.take()),
Err(std::sync::TryLockError::Poisoned(p)) => {
std::mem::forget(p.into_inner().take())
}
Err(std::sync::TryLockError::WouldBlock) => {}
}
return;
}
let conn = self.conn.lock().unwrap_or_else(|p| p.into_inner()).take();
if conn.is_some() {
self.rt.block_on(async move { drop(conn) });
}
}
pub fn is_closed(&self) -> bool {
if check_pid(self.pid).is_err() {
return match self.conn.try_lock() {
Ok(g) => g.is_none(),
Err(std::sync::TryLockError::Poisoned(p)) => p.into_inner().is_none(),
Err(std::sync::TryLockError::WouldBlock) => true,
};
}
self.conn
.lock()
.unwrap_or_else(|p| p.into_inner())
.is_none()
}
}
impl Drop for Connection {
fn drop(&mut self) {
if check_pid(self.pid).is_err() {
let slot = self.conn.get_mut().unwrap_or_else(|p| p.into_inner());
std::mem::forget(slot.take());
return;
}
self.close();
unsafe { ManuallyDrop::drop(&mut self.rt) };
}
}
#[cfg(all(test, unix))]
mod fork_lock_tests {
use super::*;
use std::sync::mpsc;
fn fork_with_lock_held(
lock_it: impl FnOnce(mpsc::Sender<()>, mpsc::Receiver<()>) + Send + 'static,
child_body: impl FnOnce(),
) {
let (held_tx, held_rx) = mpsc::channel();
let (release_tx, release_rx) = mpsc::channel();
let holder = std::thread::spawn(move || lock_it(held_tx, release_rx));
held_rx.recv().unwrap();
let pid = unsafe { libc::fork() };
assert!(pid >= 0, "fork failed");
if pid == 0 {
unsafe { libc::alarm(5) };
child_body();
unsafe { libc::_exit(0) };
}
let mut status = 0;
let waited = unsafe { libc::waitpid(pid, &mut status, 0) };
release_tx.send(()).unwrap();
holder.join().unwrap();
assert_eq!(waited, pid, "waitpid returned the wrong pid");
assert!(
libc::WIFEXITED(status),
"child did not exit normally (hung on a held lock?): {status}"
);
assert_eq!(libc::WEXITSTATUS(status), 0);
}
#[test]
fn forked_child_does_not_hang_on_database_lock_held_at_fork() {
let db = Box::new(Database::open(OpenOptions::default()).unwrap());
let addr = &*db as *const Database as usize;
fork_with_lock_held(
move |held, release| {
let db = unsafe { &*(addr as *const Database) };
let _guard = db.inner.lock().unwrap();
held.send(()).unwrap();
let _ = release.recv();
},
|| {
db.close();
assert!(db.is_closed());
std::mem::drop(db);
},
);
}
#[test]
fn forked_child_does_not_hang_on_connection_lock_held_at_fork() {
let db = Database::open(OpenOptions::default()).unwrap();
let conn = Box::new(db.connect().unwrap());
let addr = &*conn as *const Connection as usize;
fork_with_lock_held(
move |held, release| {
let conn = unsafe { &*(addr as *const Connection) };
let _guard = conn.conn.lock().unwrap();
held.send(()).unwrap();
let _ = release.recv();
},
|| {
conn.close();
assert!(conn.is_closed());
std::mem::drop(conn);
std::mem::drop(db);
},
);
}
#[test]
fn forked_child_drop_without_close_does_not_hang_on_held_lock() {
let db = Box::new(Database::open(OpenOptions::default()).unwrap());
let addr = &*db as *const Database as usize;
fork_with_lock_held(
move |held, release| {
let db = unsafe { &*(addr as *const Database) };
let _guard = db.inner.lock().unwrap();
held.send(()).unwrap();
let _ = release.recv();
},
|| std::mem::drop(db),
);
}
}