Skip to content

Commit ad8f1ec

Browse files
authored
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
1 parent cceea65 commit ad8f1ec

13 files changed

Lines changed: 721 additions & 1122 deletions

File tree

crates/cdk-sql-common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ cdk-common = { workspace = true, features = ["test"] }
2323
bitcoin.workspace = true
2424
thiserror.workspace = true
2525
tracing.workspace = true
26+
tokio.workspace = true
2627
serde.workspace = true
2728
serde_json.workspace = true
2829
lightning-invoice.workspace = true

crates/cdk-sql-common/src/common.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ use crate::stmt::query;
33

44
/// Migrates the migration generated by `build.rs`
55
#[inline(always)]
6-
pub async fn migrate<C: DatabaseExecutor>(
6+
pub async fn migrate<C>(
77
conn: &C,
88
db_prefix: &str,
99
migrations: &[(&str, &str, &str)],
10-
) -> Result<(), cdk_common::database::Error> {
10+
) -> Result<(), cdk_common::database::Error>
11+
where
12+
C: DatabaseExecutor,
13+
{
1114
query(
1215
r#"
1316
CREATE TABLE IF NOT EXISTS migrations (

crates/cdk-sql-common/src/database.rs

Lines changed: 160 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
//! Database traits definition
22
33
use std::fmt::Debug;
4+
use std::marker::PhantomData;
5+
use std::ops::{Deref, DerefMut};
46

57
use cdk_common::database::Error;
68

7-
use crate::stmt::{Column, Statement};
9+
use crate::stmt::{query, Column, Statement};
810

911
/// Database Executor
1012
///
@@ -32,22 +34,170 @@ pub trait DatabaseExecutor: Debug + Sync + Send {
3234

3335
/// Database transaction trait
3436
#[async_trait::async_trait]
35-
pub trait DatabaseTransaction<'a>: Debug + DatabaseExecutor + Send + Sync {
37+
pub trait DatabaseTransaction<DB>
38+
where
39+
DB: DatabaseExecutor,
40+
{
3641
/// Consumes the current transaction committing the changes
37-
async fn commit(self) -> Result<(), Error>;
42+
async fn commit(conn: &mut DB) -> Result<(), Error>;
43+
44+
/// Begin a transaction
45+
async fn begin(conn: &mut DB) -> Result<(), Error>;
3846

3947
/// Consumes the transaction rolling back all changes
40-
async fn rollback(self) -> Result<(), Error>;
48+
async fn rollback(conn: &mut DB) -> Result<(), Error>;
49+
}
50+
51+
/// Database connection with a transaction
52+
#[derive(Debug)]
53+
pub struct ConnectionWithTransaction<DB, W>
54+
where
55+
DB: DatabaseConnector + 'static,
56+
W: Debug + Deref<Target = DB> + DerefMut<Target = DB> + Send + Sync + 'static,
57+
{
58+
inner: Option<W>,
59+
}
60+
61+
impl<DB, W> ConnectionWithTransaction<DB, W>
62+
where
63+
DB: DatabaseConnector,
64+
W: Debug + Deref<Target = DB> + DerefMut<Target = DB> + Send + Sync + 'static,
65+
{
66+
/// Creates a new transaction
67+
pub async fn new(mut inner: W) -> Result<Self, Error> {
68+
DB::Transaction::begin(inner.deref_mut()).await?;
69+
Ok(Self { inner: Some(inner) })
70+
}
71+
72+
/// Commits the transaction consuming it and releasing the connection back to the pool (or
73+
/// disconnecting)
74+
pub async fn commit(mut self) -> Result<(), Error> {
75+
let mut conn = self
76+
.inner
77+
.take()
78+
.ok_or(Error::Internal("Missing connection".to_owned()))?;
79+
80+
DB::Transaction::commit(&mut conn).await?;
81+
82+
Ok(())
83+
}
84+
85+
/// Rollback the transaction consuming it and releasing the connection back to the pool (or
86+
/// disconnecting)
87+
pub async fn rollback(mut self) -> Result<(), Error> {
88+
let mut conn = self
89+
.inner
90+
.take()
91+
.ok_or(Error::Internal("Missing connection".to_owned()))?;
92+
93+
DB::Transaction::rollback(&mut conn).await?;
94+
95+
Ok(())
96+
}
97+
}
98+
99+
impl<DB, W> Drop for ConnectionWithTransaction<DB, W>
100+
where
101+
DB: DatabaseConnector,
102+
W: Debug + Deref<Target = DB> + DerefMut<Target = DB> + Send + Sync + 'static,
103+
{
104+
fn drop(&mut self) {
105+
if let Some(mut conn) = self.inner.take() {
106+
tokio::spawn(async move {
107+
let _ = DB::Transaction::rollback(conn.deref_mut()).await;
108+
});
109+
}
110+
}
111+
}
112+
113+
#[async_trait::async_trait]
114+
impl<DB, W> DatabaseExecutor for ConnectionWithTransaction<DB, W>
115+
where
116+
DB: DatabaseConnector,
117+
W: Debug + Deref<Target = DB> + DerefMut<Target = DB> + Send + Sync + 'static,
118+
{
119+
fn name() -> &'static str {
120+
"Transaction"
121+
}
122+
123+
/// Executes a query and returns the affected rows
124+
async fn execute(&self, statement: Statement) -> Result<usize, Error> {
125+
self.inner
126+
.as_ref()
127+
.ok_or(Error::Internal("Missing internal connection".to_owned()))?
128+
.execute(statement)
129+
.await
130+
}
131+
132+
/// Runs the query and returns the first row or None
133+
async fn fetch_one(&self, statement: Statement) -> Result<Option<Vec<Column>>, Error> {
134+
self.inner
135+
.as_ref()
136+
.ok_or(Error::Internal("Missing internal connection".to_owned()))?
137+
.fetch_one(statement)
138+
.await
139+
}
140+
141+
/// Runs the query and returns the first row or None
142+
async fn fetch_all(&self, statement: Statement) -> Result<Vec<Vec<Column>>, Error> {
143+
self.inner
144+
.as_ref()
145+
.ok_or(Error::Internal("Missing internal connection".to_owned()))?
146+
.fetch_all(statement)
147+
.await
148+
}
149+
150+
/// Fetches the first row and column from a query
151+
async fn pluck(&self, statement: Statement) -> Result<Option<Column>, Error> {
152+
self.inner
153+
.as_ref()
154+
.ok_or(Error::Internal("Missing internal connection".to_owned()))?
155+
.pluck(statement)
156+
.await
157+
}
158+
159+
/// Batch execution
160+
async fn batch(&self, statement: Statement) -> Result<(), Error> {
161+
self.inner
162+
.as_ref()
163+
.ok_or(Error::Internal("Missing internal connection".to_owned()))?
164+
.batch(statement)
165+
.await
166+
}
167+
}
168+
169+
/// Generic transaction handler for SQLite
170+
pub struct GenericTransactionHandler<W>(PhantomData<W>);
171+
172+
#[async_trait::async_trait]
173+
impl<W> DatabaseTransaction<W> for GenericTransactionHandler<W>
174+
where
175+
W: DatabaseExecutor,
176+
{
177+
/// Consumes the current transaction committing the changes
178+
async fn commit(conn: &mut W) -> Result<(), Error> {
179+
query("COMMIT")?.execute(conn).await?;
180+
Ok(())
181+
}
182+
183+
/// Begin a transaction
184+
async fn begin(conn: &mut W) -> Result<(), Error> {
185+
query("BEGIN")?.execute(conn).await?;
186+
Ok(())
187+
}
188+
189+
/// Consumes the transaction rolling back all changes
190+
async fn rollback(conn: &mut W) -> Result<(), Error> {
191+
query("ROLLBACK")?.execute(conn).await?;
192+
Ok(())
193+
}
41194
}
42195

43196
/// Database connector
44197
#[async_trait::async_trait]
45198
pub trait DatabaseConnector: Debug + DatabaseExecutor + Send + Sync {
46-
/// Transaction type for this database connection
47-
type Transaction<'a>: DatabaseTransaction<'a>
199+
/// Database static trait for the database
200+
type Transaction: DatabaseTransaction<Self>
48201
where
49-
Self: 'a;
50-
51-
/// Begin a new transaction
52-
async fn begin(&self) -> Result<Self::Transaction<'_>, Error>;
202+
Self: Sized;
53203
}

0 commit comments

Comments
 (0)