use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::time::Duration;
use ::rusqlite::types::{ToSqlOutput, ValueRef};
use ::rusqlite::{ErrorCode, OpenFlags, ToSql};
use super::{Opening, Param, SqlError};
pub(crate) const ENGINE: &str = "SQLite";
pub(crate) const INDEXED_LOG_FRAMES: Option<u64> = None;
#[derive(Debug)]
pub(crate) struct Database {
path: String,
flags: OpenFlags,
}
impl Database {
pub(crate) async fn open(location: &str, opening: Opening) -> Result<Self, SqlError> {
match opening {
Opening::ReadOnly => Self::open_read_only(location),
Opening::Shared | Opening::ProcessLocal | Opening::Staging => {
let database = Self {
path: location.to_string(),
flags: OpenFlags::default(),
};
database.open_connection()?.pragma_update_and_check(
None,
"journal_mode",
"WAL",
|_| Ok(()),
)?;
Ok(database)
}
}
}
fn open_read_only(location: &str) -> Result<Self, SqlError> {
let flags = OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX;
let in_place = Self {
path: location.to_string(),
flags,
};
let read = in_place.open_connection().and_then(|connection| {
connection.query_row("SELECT count(*) FROM sqlite_schema", [], |_| Ok(()))?;
Ok(())
});
let Err(err) = read else {
return Ok(in_place);
};
log::debug!(
"cubecl cache: {location} is not readable in place ({err}); reading it immutable"
);
Ok(Self {
path: immutable_uri(location),
flags: flags | OpenFlags::SQLITE_OPEN_URI,
})
}
pub(crate) fn connect(&self) -> Result<Connection, SqlError> {
self.open_connection().map(Connection)
}
fn open_connection(&self) -> Result<::rusqlite::Connection, SqlError> {
Ok(::rusqlite::Connection::open_with_flags(
&self.path, self.flags,
)?)
}
}
fn immutable_uri(location: &str) -> String {
let mut uri = String::from("file:");
for character in location.chars() {
match character {
'?' => uri.push_str("%3f"),
'#' => uri.push_str("%23"),
'%' => uri.push_str("%25"),
_ => uri.push(character),
}
}
uri.push_str("?immutable=1");
uri
}
impl From<::rusqlite::Error> for SqlError {
fn from(error: ::rusqlite::Error) -> Self {
let message = error.to_string();
let code = match &error {
::rusqlite::Error::SqliteFailure(failure, _) => Some(failure.code),
_ => None,
};
match code {
Some(ErrorCode::DatabaseBusy | ErrorCode::DatabaseLocked) => Self::Busy(message),
Some(ErrorCode::DatabaseCorrupt) => Self::Corrupt(message),
Some(ErrorCode::NotADatabase) => Self::NotADatabase(message),
Some(ErrorCode::ReadOnly) => Self::ReadOnly(message),
Some(ErrorCode::DiskFull) => Self::Full(message),
Some(ErrorCode::SystemIoFailure | ErrorCode::CannotOpen) => Self::Io(message),
_ => Self::Other(message),
}
}
}
impl ToSql for Param<'_> {
fn to_sql(&self) -> ::rusqlite::Result<ToSqlOutput<'_>> {
Ok(ToSqlOutput::Borrowed(match *self {
Param::Text(text) => ValueRef::Text(text.as_bytes()),
Param::Blob(blob) => ValueRef::Blob(blob),
Param::Integer(integer) => ValueRef::Integer(integer),
}))
}
}
pub(crate) struct Connection(::rusqlite::Connection);
impl Connection {
pub(crate) fn execute(&self, sql: &str, params: &[Param<'_>]) -> Result<u64, SqlError> {
let mut statement = self.0.prepare_cached(sql)?;
let changed = statement.execute(::rusqlite::params_from_iter(params))?;
Ok(changed as u64)
}
pub(crate) fn query<E: From<SqlError>>(
&self,
sql: &str,
params: &[Param<'_>],
visit: &mut dyn FnMut(&Row<'_>) -> Result<(), E>,
) -> Result<(), E> {
let mut statement = self.0.prepare_cached(sql).map_err(SqlError::from)?;
let mut rows = statement
.query(::rusqlite::params_from_iter(params))
.map_err(SqlError::from)?;
while let Some(row) = rows.next().map_err(SqlError::from)? {
visit(&Row(row))?;
}
Ok(())
}
pub(crate) fn pragma(
&self,
pragma: &str,
visit: &mut dyn FnMut(&Row<'_>),
) -> Result<(), SqlError> {
let mut statement = self.0.prepare(&alloc::format!("PRAGMA {pragma}"))?;
let mut rows = statement.query([])?;
while let Some(row) = rows.next()? {
visit(&Row(row));
}
Ok(())
}
pub(crate) fn busy_timeout(&self, timeout: Duration) -> Result<(), SqlError> {
Ok(self.0.busy_timeout(timeout)?)
}
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 ::rusqlite::Row<'r>);
impl Row<'_> {
pub(crate) fn text(&self, index: usize) -> Result<String, SqlError> {
Ok(self.0.get(index)?)
}
pub(crate) fn blob(&self, index: usize) -> Result<Vec<u8>, SqlError> {
Ok(self.0.get(index)?)
}
pub(crate) fn integer(&self, index: usize) -> Result<i64, SqlError> {
Ok(self.0.get(index)?)
}
}