Working on a better database abstraction (#931)

* Working on a better database abstraction

After [this question in the chat](https://matrix.to/#/!oJFtttFHGfnTGrIjvD:matrix.cashu.space/$oJFtttFHGfnTGrIjvD:matrix.cashu.space/$I5ZtjJtBM0ctltThDYpoCwClZFlM6PHzf8q2Rjqmso8)
regarding a database transaction within the same function, I realized a few
design flaws in our SQL database abstraction, particularly regarding
transactions.

1. Our upper abstraction got it right, where a transaction is bound with `&mut
   self`, so Rust knows how to handle its lifetime with' async/await'.
2. The raw database does not; instead, it returns &self, and beginning a
   transaction takes &self as well, which is problematic for Rust, but that's not
   all. It is fundamentally wrong. A transaction should take &mut self when
   beginning a transaction, as that connection is bound to a transaction and
   should not be returned to the pool. Currently, that responsibility lies with
   the implementor. If a mistake is made, a transaction could be executed in two
   or more connections.
3. The way a database is bound to our store layer is through a single struct,
   which may or may not internally utilize our connection pool. This is also
   another design flow, in this PR, a connection pool is owned, and to use a
   connection, it should be requested, and that connection is reference with
   mutable when beginning a transaction

* Improve the abstraction with fewer generics

As suggested by @thesimplekid

* Add BEGIN IMMEDIATE for SQLite
This commit is contained in:
C
2025-08-06 03:58:03 -03:00
committed by GitHub
parent cceea654fe
commit ad8f1ece5c
13 changed files with 721 additions and 1122 deletions

View File

@@ -0,0 +1,197 @@
//! Simple SQLite
use cdk_common::database::Error;
use cdk_sql_common::database::{DatabaseConnector, DatabaseExecutor, DatabaseTransaction};
use cdk_sql_common::stmt::{query, Column, SqlPart, Statement};
use rusqlite::{ffi, CachedStatement, Connection, Error as SqliteError, ErrorCode};
use tokio::sync::Mutex;
use crate::common::{from_sqlite, to_sqlite};
/// Async Sqlite wrapper
#[derive(Debug)]
pub struct AsyncSqlite {
inner: Mutex<Connection>,
}
impl AsyncSqlite {
pub fn new(inner: Connection) -> Self {
Self {
inner: inner.into(),
}
}
}
impl AsyncSqlite {
fn get_stmt<'a>(
&self,
conn: &'a Connection,
statement: Statement,
) -> Result<CachedStatement<'a>, Error> {
let (sql, placeholder_values) = statement.to_sql()?;
let new_sql = sql.trim().trim_end_matches("FOR UPDATE");
let mut stmt = conn
.prepare_cached(new_sql)
.map_err(|e| Error::Database(Box::new(e)))?;
for (i, value) in placeholder_values.into_iter().enumerate() {
stmt.raw_bind_parameter(i + 1, to_sqlite(value))
.map_err(|e| Error::Database(Box::new(e)))?;
}
Ok(stmt)
}
}
#[inline(always)]
fn to_sqlite_error(err: SqliteError) -> Error {
tracing::error!("Failed query with error {:?}", err);
if let rusqlite::Error::SqliteFailure(
ffi::Error {
code,
extended_code,
},
_,
) = err
{
if code == ErrorCode::ConstraintViolation
&& (extended_code == ffi::SQLITE_CONSTRAINT_PRIMARYKEY
|| extended_code == ffi::SQLITE_CONSTRAINT_UNIQUE)
{
Error::Duplicate
} else {
Error::Database(Box::new(err))
}
} else {
Error::Database(Box::new(err))
}
}
/// SQLite trasanction handler
pub struct SQLiteTransactionHandler;
#[async_trait::async_trait]
impl DatabaseTransaction<AsyncSqlite> for SQLiteTransactionHandler {
/// Consumes the current transaction committing the changes
async fn commit(conn: &mut AsyncSqlite) -> Result<(), Error> {
query("COMMIT")?.execute(conn).await?;
Ok(())
}
/// Begin a transaction
async fn begin(conn: &mut AsyncSqlite) -> Result<(), Error> {
query("BEGIN IMMEDIATE")?.execute(conn).await?;
Ok(())
}
/// Consumes the transaction rolling back all changes
async fn rollback(conn: &mut AsyncSqlite) -> Result<(), Error> {
query("ROLLBACK")?.execute(conn).await?;
Ok(())
}
}
impl DatabaseConnector for AsyncSqlite {
type Transaction = SQLiteTransactionHandler;
}
#[async_trait::async_trait]
impl DatabaseExecutor for AsyncSqlite {
fn name() -> &'static str {
"sqlite"
}
async fn execute(&self, statement: Statement) -> Result<usize, Error> {
let conn = self.inner.lock().await;
let mut stmt = self
.get_stmt(&conn, statement)
.map_err(|e| Error::Database(Box::new(e)))?;
Ok(stmt.raw_execute().map_err(to_sqlite_error)?)
}
async fn fetch_one(&self, statement: Statement) -> Result<Option<Vec<Column>>, Error> {
let conn = self.inner.lock().await;
let mut stmt = self
.get_stmt(&conn, statement)
.map_err(|e| Error::Database(Box::new(e)))?;
let columns = stmt.column_count();
let mut rows = stmt.raw_query();
rows.next()
.map_err(to_sqlite_error)?
.map(|row| {
(0..columns)
.map(|i| row.get(i).map(from_sqlite))
.collect::<Result<Vec<_>, _>>()
})
.transpose()
.map_err(to_sqlite_error)
}
async fn fetch_all(&self, statement: Statement) -> Result<Vec<Vec<Column>>, Error> {
let conn = self.inner.lock().await;
let mut stmt = self
.get_stmt(&conn, statement)
.map_err(|e| Error::Database(Box::new(e)))?;
let columns = stmt.column_count();
let mut rows = stmt.raw_query();
let mut results = vec![];
while let Some(row) = rows.next().map_err(to_sqlite_error)? {
results.push(
(0..columns)
.map(|i| row.get(i).map(from_sqlite))
.collect::<Result<Vec<_>, _>>()
.map_err(to_sqlite_error)?,
)
}
Ok(results)
}
async fn pluck(&self, statement: Statement) -> Result<Option<Column>, Error> {
let conn = self.inner.lock().await;
let mut stmt = self
.get_stmt(&conn, statement)
.map_err(|e| Error::Database(Box::new(e)))?;
let mut rows = stmt.raw_query();
rows.next()
.map_err(to_sqlite_error)?
.map(|row| row.get(0usize).map(from_sqlite))
.transpose()
.map_err(to_sqlite_error)
}
async fn batch(&self, mut statement: Statement) -> Result<(), Error> {
let sql = {
let part = statement
.parts
.pop()
.ok_or(Error::Internal("Empty SQL".to_owned()))?;
if !statement.parts.is_empty() || matches!(part, SqlPart::Placeholder(_, _)) {
return Err(Error::Internal(
"Invalid usage, batch does not support placeholders".to_owned(),
));
}
if let SqlPart::Raw(sql) = part {
sql
} else {
unreachable!()
}
};
self.inner
.lock()
.await
.execute_batch(&sql)
.map_err(to_sqlite_error)
}
}