use crate::config::DatabaseConfig;
use crate::dialect::{Dialect, Sqlite};
use crate::driver::{BoxFuture, Driver, DriverConnection, QueryResult};
use crate::row::{Columns, Row};
use crate::value::Value;
use rustlavel_core::{Error, Result};
use rusqlite::types::{ToSqlOutput, ValueRef};
use std::sync::Arc;
use std::time::Duration;
pub use crate::config::SQLITE_IN_MEMORY as IN_MEMORY;
pub struct SqliteDriver {
config: DatabaseConfig,
dialect: Arc<dyn Dialect>,
target: String,
keeper: std::sync::Mutex<Option<rusqlite::Connection>>,
}
impl SqliteDriver {
pub fn new(config: DatabaseConfig) -> SqliteDriver {
let in_memory = config.database == IN_MEMORY || config.database.is_empty();
let target = if in_memory { shared_memory_uri() } else { config.database.clone() };
SqliteDriver {
config,
dialect: Arc::new(Sqlite),
target,
keeper: std::sync::Mutex::new(None),
}
}
pub fn path(&self) -> &str {
&self.config.database
}
pub fn is_in_memory(&self) -> bool {
self.config.database == IN_MEMORY || self.config.database.is_empty()
}
}
impl Driver for SqliteDriver {
fn dialect(&self) -> Arc<dyn Dialect> {
self.dialect.clone()
}
fn connect(&self) -> BoxFuture<'_, Result<Box<dyn DriverConnection>>> {
let target = self.target.clone();
let in_memory = self.is_in_memory();
let busy = self.config.connect_timeout;
Box::pin(async move {
let connection = {
let target = target.clone();
tokio::task::spawn_blocking(move || open(&target, in_memory, busy))
.await
.map_err(|error| {
Error::msg(format!("opening a SQLite database panicked: {error}"))
})??
};
if in_memory && self.keeper.lock().is_ok_and(|keeper| keeper.is_none()) {
let spare = {
let target = target.clone();
tokio::task::spawn_blocking(move || open(&target, true, busy))
.await
.ok()
.and_then(Result::ok)
};
if let Ok(mut keeper) = self.keeper.lock()
&& keeper.is_none()
{
*keeper = spare;
}
}
Ok(Box::new(SqliteConnection {
connection: Some(connection),
in_transaction: false,
broken: false,
}) as Box<dyn DriverConnection>)
})
}
fn describe(&self) -> String {
format!("sqlite://{}", self.config.database)
}
fn max_connections(&self) -> usize {
if self.is_in_memory() {
return 1;
}
self.config.max_connections.max(1)
}
}
fn shared_memory_uri() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
format!(
"file:rustlavel-{}-{}?mode=memory&cache=shared",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
)
}
fn open(path: &str, in_memory: bool, busy: Duration) -> Result<rusqlite::Connection> {
let connection = if in_memory {
rusqlite::Connection::open_with_flags(
path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE
| rusqlite::OpenFlags::SQLITE_OPEN_CREATE
| rusqlite::OpenFlags::SQLITE_OPEN_URI,
)
} else {
rusqlite::Connection::open(path)
}
.map_err(|error| Error::msg(format!("cannot open the SQLite database `{path}`: {error}")))?;
connection
.execute_batch("pragma foreign_keys = on")
.map_err(|error| Error::msg(format!("cannot enable foreign keys: {error}")))?;
if !in_memory {
let _ = connection.execute_batch("pragma journal_mode = wal");
}
connection
.busy_timeout(busy)
.map_err(|error| Error::msg(format!("cannot set the busy timeout: {error}")))?;
Ok(connection)
}
pub struct SqliteConnection {
connection: Option<rusqlite::Connection>,
in_transaction: bool,
broken: bool,
}
impl SqliteConnection {
async fn with<T, F>(&mut self, work: F) -> Result<T>
where
T: Send + 'static,
F: FnOnce(&rusqlite::Connection) -> Result<T> + Send + 'static,
{
let Some(connection) = self.connection.take() else {
self.broken = true;
return Err(Error::msg(
"this SQLite connection was lost by an earlier panic and cannot be used",
));
};
let handle = tokio::task::spawn_blocking(move || {
let result = work(&connection);
(connection, result)
})
.await;
match handle {
Ok((connection, result)) => {
self.connection = Some(connection);
if result.is_err() {
}
result
}
Err(error) => {
self.broken = true;
Err(Error::msg(format!("a SQLite statement panicked: {error}")))
}
}
}
}
impl DriverConnection for SqliteConnection {
fn query<'a>(&'a mut self, sql: &'a str, params: &'a [Value]) -> BoxFuture<'a, Result<QueryResult>> {
let sql = sql.to_string();
let params = params.to_vec();
Box::pin(async move {
let result = self.with(move |connection| run(connection, &sql, ¶ms)).await?;
self.in_transaction = connection_in_transaction(self);
Ok(result)
})
}
fn simple_query<'a>(&'a mut self, sql: &'a str) -> BoxFuture<'a, Result<QueryResult>> {
let sql = sql.to_string();
Box::pin(async move {
let result = self.with(move |connection| run(connection, &sql, &[])).await?;
self.in_transaction = connection_in_transaction(self);
Ok(result)
})
}
fn is_broken(&self) -> bool {
self.broken || self.connection.is_none()
}
fn in_transaction(&self) -> bool {
self.in_transaction
}
fn close(self: Box<Self>) -> BoxFuture<'static, ()> {
Box::pin(async move {
if let Some(connection) = self.connection {
let _ = tokio::task::spawn_blocking(move || {
let _ = connection.close();
})
.await;
}
})
}
}
fn connection_in_transaction(connection: &SqliteConnection) -> bool {
connection.connection.as_ref().is_some_and(|c| !c.is_autocommit())
}
fn run(connection: &rusqlite::Connection, sql: &str, params: &[Value]) -> Result<QueryResult> {
let mut statement = connection
.prepare(sql)
.map_err(|error| Error::msg(format!("{error}\n in: {sql}")))?;
let bound: Vec<Bound> = params.iter().map(Bound::from_value).collect();
let as_sql: Vec<&dyn rusqlite::ToSql> = bound.iter().map(|b| b as &dyn rusqlite::ToSql).collect();
if statement.column_count() == 0 {
let affected = statement
.execute(as_sql.as_slice())
.map_err(|error| Error::msg(format!("{error}\n in: {sql}")))?;
return Ok(QueryResult {
rows: Vec::new(),
affected: affected as u64,
last_insert_id: Some(connection.last_insert_rowid()),
});
}
let columns: Columns = Arc::new(statement.column_names().into_iter().map(str::to_string).collect());
let mut rows = Vec::new();
let mut cursor = statement
.query(as_sql.as_slice())
.map_err(|error| Error::msg(format!("{error}\n in: {sql}")))?;
while let Some(row) = cursor.next().map_err(|error| Error::msg(error.to_string()))? {
let mut values = Vec::with_capacity(columns.len());
for index in 0..columns.len() {
values.push(read(row, index)?);
}
rows.push(Row::new(columns.clone(), values));
}
Ok(QueryResult { rows, affected: 0, last_insert_id: None })
}
fn read(row: &rusqlite::Row<'_>, index: usize) -> Result<Value> {
let raw = row.get_ref(index).map_err(|error| Error::msg(error.to_string()))?;
Ok(match raw {
ValueRef::Null => Value::Null,
ValueRef::Integer(number) => Value::Int(number),
ValueRef::Real(number) => Value::Float(number),
ValueRef::Text(bytes) => Value::Text(String::from_utf8_lossy(bytes).into_owned()),
ValueRef::Blob(bytes) => Value::Bytes(bytes.to_vec()),
})
}
enum Bound {
Null,
Int(i64),
Float(f64),
Text(String),
Bytes(Vec<u8>),
}
impl Bound {
fn from_value(value: &Value) -> Bound {
match value {
Value::Null => Bound::Null,
Value::Bool(flag) => Bound::Int(i64::from(*flag)),
Value::Int(number) => Bound::Int(*number),
Value::Float(number) => Bound::Float(*number),
Value::Text(text) => Bound::Text(text.clone()),
Value::Bytes(bytes) => Bound::Bytes(bytes.clone()),
Value::Json(json) => Bound::Text(json.to_string()),
}
}
}
impl rusqlite::ToSql for Bound {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(match self {
Bound::Null => ToSqlOutput::Borrowed(ValueRef::Null),
Bound::Int(number) => ToSqlOutput::Borrowed(ValueRef::Integer(*number)),
Bound::Float(number) => ToSqlOutput::Borrowed(ValueRef::Real(*number)),
Bound::Text(text) => ToSqlOutput::Borrowed(ValueRef::Text(text.as_bytes())),
Bound::Bytes(bytes) => ToSqlOutput::Borrowed(ValueRef::Blob(bytes)),
})
}
}