use std::sync::Arc;
use std::time::Duration;
use turso::core::OpenFlags;
use turso::params::Params;
use turso::{IntoParams, Value};
use turso_sdk_kit::rsapi::{
FromValue, TursoConnection, TursoDatabase, TursoDatabaseConfig, TursoError, TursoStatement,
TursoStatusCode, Value as CoreValue,
};
use crate::util::UnwrapPoison;
const STATEMENT_LIMIT_DEFAULT_SECS: u64 = 180;
pub(crate) const STATEMENT_LIMIT_ENV: &str = "MAHBOT_STORE_STATEMENT_LIMIT_SECS";
#[must_use]
pub(crate) fn statement_limit() -> Duration {
crate::util::env_duration_secs(STATEMENT_LIMIT_ENV, STATEMENT_LIMIT_DEFAULT_SECS)
.max(Duration::from_secs(1))
}
#[must_use]
pub(crate) fn too_heavy_message() -> String {
format!(
"the statement is too heavy to run: it held the store for longer than its {}s \
per-statement limit and was terminated — reformulate it (narrow the scan, add a filter, \
use an index or add a LIMIT) instead of sending the same query again",
statement_limit().as_secs()
)
}
fn map_error(err: TursoError) -> turso::Error {
match err {
TursoError::Interrupt(_) => turso::Error::Interrupt(too_heavy_message()),
other => other.into(),
}
}
fn experimental_features() -> Option<String> {
(!crate::db::EXPERIMENTAL_FEATURES.is_empty())
.then(|| crate::db::EXPERIMENTAL_FEATURES.join(","))
}
fn database_config(path: &str) -> TursoDatabaseConfig {
TursoDatabaseConfig {
path: path.to_owned(),
experimental_features: experimental_features(),
async_io: true,
encryption: None,
vfs: turso::IoBackend::Default,
io: None,
db_file: None,
page_codec: None,
open_flags: OpenFlags::default(),
}
}
impl std::fmt::Debug for Connection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Connection").finish()
}
}
pub(crate) struct Database {
inner: Arc<TursoDatabase>,
}
impl std::fmt::Debug for Database {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Database").finish()
}
}
impl Database {
pub(crate) async fn open(path: &str) -> turso::Result<Self> {
let db = TursoDatabase::new(database_config(path));
loop {
let opened = db.open().map_err(map_error)?;
let Some(completion) = opened.io() else {
break;
};
let io = db.io().map_err(map_error)?;
completion
.wait_async(io.as_ref())
.await
.map_err(TursoError::from)
.map_err(map_error)?;
}
Ok(Self { inner: db })
}
pub(crate) fn connect(&self) -> turso::Result<Connection> {
self.connect_with(statement_limit())
}
fn connect_with(&self, limit: Duration) -> turso::Result<Connection> {
let conn = self.inner.connect().map_err(map_error)?;
conn.set_query_timeout(limit);
Ok(Connection { inner: conn })
}
}
pub(crate) struct Connection {
inner: Arc<TursoConnection>,
}
impl Connection {
pub(crate) fn query(&self, sql: &str, params: impl IntoParams) -> turso::Result<Rows> {
self.prepare(sql)?.query(params)
}
pub(crate) async fn execute(&self, sql: &str, params: impl IntoParams) -> turso::Result<u64> {
let mut stmt = self.prepare(sql)?;
stmt.execute(params).await
}
pub(crate) async fn execute_batch(&self, sql: &str) -> turso::Result<()> {
let mut sql = sql;
while let Some((stmt, offset)) = self.inner.prepare_first(sql).map_err(map_error)? {
let mut stmt = statement(stmt);
stmt.execute(()).await?;
sql = &sql[offset..];
}
Ok(())
}
pub(crate) fn prepare_cached(&self, sql: &str) -> turso::Result<Statement> {
let inner = self.inner.prepare_cached(sql).map_err(map_error)?;
Ok(statement(inner))
}
pub(crate) fn busy_timeout(&self, duration: Duration) {
self.inner.set_busy_timeout(duration);
}
#[must_use]
pub(crate) fn is_autocommit(&self) -> bool {
self.inner.get_auto_commit()
}
fn prepare(&self, sql: &str) -> turso::Result<Statement> {
let inner = self.inner.prepare_single(sql).map_err(map_error)?;
Ok(statement(inner))
}
}
fn statement(inner: Box<TursoStatement>) -> Statement {
Statement {
inner: Arc::new(std::sync::Mutex::new(inner)),
}
}
#[derive(Clone)]
pub(crate) struct Statement {
inner: Arc<std::sync::Mutex<Box<TursoStatement>>>,
}
impl Statement {
pub(crate) fn query(&mut self, params: impl IntoParams) -> turso::Result<Rows> {
self.bind(params)?;
let columns = self.column_count();
Ok(Rows {
stmt: self.clone(),
columns,
})
}
pub(crate) async fn execute(&mut self, params: impl IntoParams) -> turso::Result<u64> {
self.bind(params)?;
match self.step().await? {
TursoStatusCode::Done => Ok(self.change_count()),
TursoStatusCode::Row => Err(turso::Error::Misuse(
"unexpected row during execution".to_string(),
)),
TursoStatusCode::Io => unreachable!("step() runs the engine's IO"),
}
}
async fn step(&self) -> turso::Result<TursoStatusCode> {
loop {
let status = self
.inner
.lock()
.unwrap_poison()
.step(None)
.map_err(map_error)?;
if status != TursoStatusCode::Io {
return Ok(status);
}
self.inner
.lock()
.unwrap_poison()
.run_io()
.map_err(map_error)?;
tokio::task::yield_now().await;
}
}
fn bind(&mut self, params: impl IntoParams) -> turso::Result<()> {
let params = params.into_params()?;
let mut inner = self.inner.lock().unwrap_poison();
inner.reset().map_err(map_error)?;
match params {
Params::None => {}
Params::Positional(values) => {
for (index, value) in values.into_iter().enumerate() {
inner
.bind_positional(index + 1, value.into())
.map_err(map_error)?;
}
}
Params::Named(values) => {
for (name, value) in values {
let position = inner.named_position(name).map_err(map_error)?;
inner
.bind_positional(position, value.into())
.map_err(map_error)?;
}
}
}
Ok(())
}
fn change_count(&self) -> u64 {
let inner = self.inner.lock().unwrap_poison();
inner.n_change().try_into().unwrap_or(0)
}
fn column_count(&self) -> usize {
self.inner.lock().unwrap_poison().column_count()
}
fn column_names(&self) -> Vec<String> {
let inner = self.inner.lock().unwrap_poison();
(0..inner.column_count())
.map(|index| inner.column_name(index).unwrap_or_default())
.collect()
}
fn row(&self, columns: usize) -> turso::Result<Row> {
let inner = self.inner.lock().unwrap_poison();
let mut values = Vec::with_capacity(columns);
for index in 0..columns {
values.push(inner.row_value(index).map_err(map_error)?);
}
Ok(Row { values })
}
}
pub(crate) struct Rows {
stmt: Statement,
columns: usize,
}
impl Rows {
#[must_use]
pub(crate) fn column_names(&self) -> Vec<String> {
self.stmt.column_names()
}
pub(crate) async fn next(&mut self) -> turso::Result<Option<Row>> {
match self.stmt.step().await? {
TursoStatusCode::Done => Ok(None),
TursoStatusCode::Row => Ok(Some(self.stmt.row(self.columns)?)),
TursoStatusCode::Io => unreachable!("step() runs the engine's IO"),
}
}
}
#[derive(Debug)]
pub(crate) struct Row {
values: Vec<CoreValue>,
}
impl Row {
pub(crate) fn get_value(&self, index: usize) -> turso::Result<Value> {
Ok(self.value(index)?.clone().into())
}
pub(crate) fn get<T: FromValue>(&self, index: usize) -> turso::Result<T> {
T::from_sql(self.value(index)?.clone())
.map_err(|err| turso::Error::ConversionFailure(err.to_string()))
}
#[must_use]
pub(crate) fn column_count(&self) -> usize {
self.values.len()
}
fn value(&self, index: usize) -> turso::Result<&CoreValue> {
self.values.get(index).ok_or_else(|| {
turso::Error::Misuse(format!(
"column index {index} out of bounds (row has {} columns)",
self.values.len()
))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn feature_string_matches_the_debug_paths_typed_opts() {
assert_eq!(
database_config(":memory:").database_opts(),
crate::db::experimental_database_opts()
);
}
#[tokio::test]
async fn a_statement_past_the_limit_is_cut_and_the_store_stays_usable() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("cut.db");
let db = Database::open(path.to_str().expect("utf-8 path"))
.await
.expect("open store");
let conn = db
.connect_with(Duration::from_secs(1))
.expect("connect with a one-second limit");
conn.execute("CREATE TABLE big (x INTEGER)", ())
.await
.expect("create table");
conn.execute("INSERT INTO big (x) VALUES (1)", ())
.await
.expect("seed a row");
let mut cut = conn
.query(&format!("{HEAVY}SELECT COUNT(*) FROM c"), ())
.expect("prepare the heavy statement");
let err = cut
.next()
.await
.expect_err("the statement must be cut at the limit");
assert!(
matches!(err, turso::Error::Interrupt(_)),
"a cut statement must be reported as an interrupt: {err:?}"
);
assert!(
err.to_string().contains("too heavy"),
"the caller must be told the query is too heavy: {err}"
);
let mut rows = conn
.query("SELECT COUNT(*) FROM big", ())
.expect("the store answers the next operation");
let row = rows.next().await.expect("row").expect("one aggregate row");
assert_eq!(row.get::<i64>(0).expect("count"), 1);
conn.execute("INSERT INTO big (x) VALUES (2)", ())
.await
.expect("the connection still writes after a cut");
let err = conn
.execute(&format!("{HEAVY}INSERT INTO big (x) SELECT i FROM c"), ())
.await
.expect_err("the write must be cut at the limit");
assert!(
err.to_string().contains("too heavy"),
"a cut write must be told the same thing: {err}"
);
let mut rows = conn
.query("SELECT COUNT(*) FROM big", ())
.expect("the store answers after a cut write");
let row = rows.next().await.expect("row").expect("one aggregate row");
assert_eq!(row.get::<i64>(0).expect("count"), 2);
}
const HEAVY: &str =
"WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i < 100000000) ";
}