use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::future::Future;
use core::time::Duration;
use super::{Opening, Param, SqlError};
pub(crate) const ENGINE: &str = "Turso";
#[cfg_attr(
browser_cache,
expect(dead_code, reason = "one tab writes the browser's files")
)]
pub(crate) const INDEXED_LOG_FRAMES: Option<u64> = Some(262_144);
#[derive(Debug)]
pub(crate) struct Database(::turso::Database);
impl Database {
#[cfg(native_cache)]
pub(crate) async fn open(location: &str, opening: Opening) -> Result<Self, SqlError> {
let builder = ::turso::Builder::new_local(location);
let opened = match opening {
Opening::ReadOnly => builder.read_only(true).build().await,
Opening::ProcessLocal => builder.experimental_multiprocess_wal(false).build().await,
Opening::Staging => builder.experimental_vacuum(true).build().await,
Opening::Shared => open_shared(location).await,
};
opened.map(Self).map_err(SqlError::from)
}
#[cfg(browser_cache)]
pub(crate) async fn open(location: &str, opening: Opening) -> Result<Self, SqlError> {
let _ = opening;
let wal = alloc::format!("{location}-wal");
let io = crate::persistence::turso_browser::BrowserIo::new(&[location, &wal])
.await
.map_err(SqlError::Other)?;
::turso::Builder::new_local(location)
.with_io_impl(alloc::sync::Arc::new(io))
.build()
.await
.map(Self)
.map_err(SqlError::from)
}
pub(crate) fn connect(&self) -> Result<Connection, SqlError> {
self.0.connect().map(Connection).map_err(SqlError::from)
}
}
#[cfg(native_cache)]
async fn open_shared(location: &str) -> Result<::turso::Database, ::turso::Error> {
if cfg!(windows) {
log::info!("cubecl cache: opening {location} for this process alone");
return open_writable(location, false).await;
}
match open_writable(location, true).await {
Err(err) if multiprocess_wal_unsupported(&err) => {
log::info!("cubecl cache: {err}; opening {location} for this process alone");
open_writable(location, false).await
}
opened => opened,
}
}
#[cfg(native_cache)]
async fn open_writable(
location: &str,
multiprocess: bool,
) -> Result<::turso::Database, ::turso::Error> {
::turso::Builder::new_local(location)
.experimental_multiprocess_wal(multiprocess)
.build()
.await
}
#[cfg(native_cache)]
fn multiprocess_wal_unsupported(err: &::turso::Error) -> bool {
matches!(err, ::turso::Error::Error(message) if message.contains("multiprocess WAL is not supported"))
}
fn held_by_another_process(message: &str) -> bool {
message.contains("in another process") || message.contains("locked by another process")
}
impl From<::turso::Error> for SqlError {
fn from(error: ::turso::Error) -> Self {
let message = error.to_string();
match error {
::turso::Error::Busy(_) | ::turso::Error::BusySnapshot(_) => Self::Busy(message),
::turso::Error::Error(_) if held_by_another_process(&message) => {
Self::HeldByAnotherProcess(message)
}
::turso::Error::Corrupt(_) => Self::Corrupt(message),
::turso::Error::NotAdb(_) => Self::NotADatabase(message),
::turso::Error::Readonly(_) => Self::ReadOnly(message),
::turso::Error::DatabaseFull(_) => Self::Full(message),
::turso::Error::IoError(..) => Self::Io(message),
_ => Self::Other(message),
}
}
}
pub(crate) struct Connection(::turso::Connection);
impl Connection {
pub(crate) fn execute(&self, sql: &str, params: &[Param<'_>]) -> Result<u64, SqlError> {
drive(self.0.execute(sql, values(params))).map_err(SqlError::from)
}
pub(crate) fn query<E: From<SqlError>>(
&self,
sql: &str,
params: &[Param<'_>],
visit: &mut dyn FnMut(&Row<'_>) -> Result<(), E>,
) -> Result<(), E> {
let mut rows = drive(self.0.query(sql, values(params))).map_err(SqlError::from)?;
while let Some(row) = drive(rows.next()).map_err(SqlError::from)? {
visit(&Row(&row))?;
}
Ok(())
}
pub(crate) fn pragma(
&self,
pragma: &str,
visit: &mut dyn FnMut(&Row<'_>),
) -> Result<(), SqlError> {
drive(self.0.pragma_query(pragma, |row| {
visit(&Row(row));
Ok(())
}))
.map_err(SqlError::from)
}
pub(crate) fn busy_timeout(&self, timeout: Duration) -> Result<(), SqlError> {
self.0.busy_timeout(timeout).map_err(SqlError::from)
}
#[cfg(native_cache)]
pub(crate) fn checkpoint(&self) -> Result<bool, SqlError> {
let mut complete = true;
self.pragma("wal_checkpoint(TRUNCATE)", &mut |row| {
complete &= row.integer(0).is_ok_and(|busy| busy == 0);
})?;
Ok(complete)
}
}
pub(crate) struct Row<'r>(&'r ::turso::Row);
impl Row<'_> {
pub(crate) fn text(&self, index: usize) -> Result<String, SqlError> {
self.0.get(index).map_err(SqlError::from)
}
pub(crate) fn blob(&self, index: usize) -> Result<Vec<u8>, SqlError> {
self.0.get(index).map_err(SqlError::from)
}
pub(crate) fn integer(&self, index: usize) -> Result<i64, SqlError> {
self.0.get(index).map_err(SqlError::from)
}
}
fn values(params: &[Param<'_>]) -> Vec<::turso::Value> {
params
.iter()
.map(|param| match *param {
Param::Text(text) => ::turso::Value::Text(text.to_string()),
Param::Blob(blob) => ::turso::Value::Blob(blob.to_vec()),
Param::Integer(integer) => ::turso::Value::Integer(integer),
})
.collect()
}
fn drive<T>(future: impl Future<Output = T>) -> T {
#[cfg(not(browser_cache))]
{
crate::future::block_on(future)
}
#[cfg(browser_cache)]
{
use core::pin::pin;
use core::task::{Context, Poll, Waker};
let mut future = pin!(future);
let mut context = Context::from_waker(Waker::noop());
loop {
if let Poll::Ready(output) = future.as_mut().poll(&mut context) {
return output;
}
}
}
}
#[cfg(all(test, native_cache))]
mod tests {
use super::*;
use crate::future::block_on;
#[test_log::test]
#[cfg_attr(miri, ignore)]
fn a_location_without_a_shared_wal_opens_for_this_process() {
let database = block_on(Database::open(":memory:", Opening::Shared)).unwrap();
let connection = database.connect().unwrap();
connection
.execute("CREATE TABLE notes (body TEXT)", &[])
.unwrap();
assert_eq!(
connection
.execute("INSERT INTO notes VALUES (?1)", &["written".into()])
.unwrap(),
1
);
}
#[test_log::test]
#[cfg_attr(miri, ignore)]
#[cfg(all(any(unix, windows), target_pointer_width = "64"))]
fn the_engine_declines_a_shared_wal_in_words_we_recognize() {
let Err(err) = block_on(open_writable(":memory:", true)) else {
panic!("an in-memory database must not get a shared WAL");
};
assert!(
multiprocess_wal_unsupported(&err),
"unrecognized refusal: {err}"
);
}
}