Skip to main content

turso_orm_driver/
transaction.rs

1//! Transactions bound to one pooled connection, modeled by [`Transaction`].
2//!
3//! SQLite transactions are a property of the connection, so a transaction
4//! must pin the pooled connection it started on and route every statement
5//! through it; a second connection would see a different snapshot or block
6//! on the first one's lock. Nesting is done with `SAVEPOINT`s on that same
7//! connection, which is why a nested [`Transaction`] shares the parent's
8//! state rather than acquiring anything.
9//!
10//! `Drop` cannot await, so an unfinished transaction cannot roll itself
11//! back synchronously. The module makes that safe in two ways: a dropped
12//! top-level transaction discards its connection, which the engine rolls
13//! back when the connection closes and which the pool never hands out
14//! again; a dropped nested transaction records its depth in the shared
15//! state and the parent runs `ROLLBACK TO SAVEPOINT` before its next
16//! statement. The shallowest dropped depth wins because rolling back to it
17//! subsumes every deeper savepoint.
18//!
19//! This module owns the transaction lifecycle only. Statement execution is
20//! delegated to `crate::executor` and the pool to `crate::database`.
21//!
22//! - [`Transaction`]: the handle, which must be committed or rolled back;
23//! - [`TransactionMode`]: how a top-level transaction takes its locks.
24
25use std::fmt;
26use std::future::Future;
27use std::pin::Pin;
28use std::sync::Arc;
29use std::sync::atomic::{AtomicU32, Ordering};
30
31use async_trait::async_trait;
32use turso_sql::Statement;
33
34use crate::connection::{ConnectionTrait, StreamTrait, TransactionTrait};
35use crate::database::{PooledConnection, retry_busy};
36use crate::error::Result;
37use crate::executor::{self, Conn, ExecResult, Row, RowStream};
38
39/// The sentinel stored in `Shared::pending_rollback` when no nested
40/// transaction was dropped. `u32::MAX` so that `fetch_min` with any real
41/// depth replaces it.
42const NO_PENDING: u32 = u32::MAX;
43
44/// How a top-level transaction is started.
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
46#[non_exhaustive]
47pub enum TransactionMode {
48    /// `BEGIN DEFERRED`, the default: locks are taken by the first
49    /// statement.
50    #[default]
51    Deferred,
52    /// `BEGIN IMMEDIATE`: the write lock is taken now, so a writer finds out
53    /// about contention before doing any work.
54    Immediate,
55    /// `BEGIN EXCLUSIVE`: the exclusive lock is taken now.
56    Exclusive,
57    /// `BEGIN CONCURRENT`: an optimistic MVCC transaction whose conflicts
58    /// surface at commit. Requires
59    /// [`ConnectOptions::mvcc`](crate::ConnectOptions::mvcc).
60    Concurrent,
61}
62
63impl TransactionMode {
64    /// The `BEGIN` statement for this mode.
65    fn sql(self) -> &'static str {
66        match self {
67            Self::Deferred => "BEGIN DEFERRED",
68            Self::Immediate => "BEGIN IMMEDIATE",
69            Self::Exclusive => "BEGIN EXCLUSIVE",
70            Self::Concurrent => "BEGIN CONCURRENT",
71        }
72    }
73}
74
75/// The connection state shared by a transaction and its nested savepoints.
76struct Shared {
77    /// The pinned connection.
78    conn: PooledConnection,
79    /// The depth of the deepest savepoint still open.
80    depth: AtomicU32,
81    /// The depth of the shallowest nested transaction dropped without
82    /// commit or rollback, or [`NO_PENDING`]; rolled back to before the
83    /// next statement.
84    pending_rollback: AtomicU32,
85}
86
87impl Shared {
88    /// Applies the rollback owed by a dropped nested transaction, if any.
89    ///
90    /// Called before every statement and before finishing, so that work
91    /// done inside a dropped savepoint never leaks into the parent.
92    ///
93    /// # Errors
94    ///
95    /// Returns [`Error::Turso`](crate::Error::Turso) when the engine cannot
96    /// roll back to or release the savepoint.
97    async fn settle(&self) -> Result<()> {
98        let pending = self.pending_rollback.swap(NO_PENDING, Ordering::AcqRel);
99        if pending != NO_PENDING {
100            tracing::warn!(
101                depth = pending,
102                "rolling back nested transaction dropped without commit"
103            );
104            rollback_to(&self.conn, pending).await?;
105            self.depth.store(pending - 1, Ordering::Release);
106        }
107        Ok(())
108    }
109}
110
111/// Rolls back to the savepoint at `depth` and releases it.
112///
113/// `ROLLBACK TO` alone leaves the savepoint on the stack, so it is released
114/// afterwards to keep the engine's savepoint stack in step with `depth`.
115///
116/// # Errors
117///
118/// Returns [`Error::Turso`](crate::Error::Turso) when either statement
119/// fails.
120async fn rollback_to(conn: &Conn, depth: u32) -> Result<()> {
121    conn.execute_raw(&format!("ROLLBACK TO SAVEPOINT sp{depth}"))
122        .await?;
123    conn.execute_raw(&format!("RELEASE SAVEPOINT sp{depth}"))
124        .await?;
125    Ok(())
126}
127
128/// A transaction on one Turso connection.
129///
130/// Created with [`Database::begin`](crate::Database::begin) or
131/// [`Database::begin_with_mode`](crate::Database::begin_with_mode). Nested
132/// transactions (`txn.begin()`) are `SAVEPOINT`s on the same connection.
133/// Dropping an unfinished transaction rolls it back: a top-level transaction
134/// discards its connection, a nested one is rolled back to its savepoint
135/// before the parent runs its next statement.
136#[must_use = "a transaction must be committed or rolled back"]
137pub struct Transaction {
138    /// The connection state shared with the parent and children.
139    shared: Arc<Shared>,
140    /// This transaction's savepoint depth; `0` for the top level.
141    depth: u32,
142    /// Whether the transaction is still unfinished; cleared by `finish` so
143    /// that `Drop` does nothing after a commit or rollback.
144    open: bool,
145}
146
147impl fmt::Debug for Transaction {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        f.debug_struct("Transaction")
150            .field("depth", &self.depth)
151            .field("open", &self.open)
152            .finish_non_exhaustive()
153    }
154}
155
156impl Transaction {
157    /// Begins a top-level transaction on `conn` with the given mode.
158    ///
159    /// `BEGIN IMMEDIATE` and `BEGIN EXCLUSIVE` fail immediately when another
160    /// writer holds the lock, so the statement is retried for the busy
161    /// timeout; a `None` busy timeout gives a zero budget and no retry.
162    ///
163    /// # Errors
164    ///
165    /// Returns [`Error::Turso`](crate::Error::Turso) when the engine cannot
166    /// start the transaction — with
167    /// [`ErrorKind::Busy`](crate::ErrorKind::Busy) once the retry budget is
168    /// exhausted.
169    pub(crate) async fn begin_top(conn: PooledConnection, mode: TransactionMode) -> Result<Self> {
170        let budget = conn.options().busy_timeout.unwrap_or_default();
171        retry_busy(budget, || async { conn.execute_raw(mode.sql()).await }).await?;
172        Ok(Self {
173            shared: Arc::new(Shared {
174                conn,
175                depth: AtomicU32::new(0),
176                pending_rollback: AtomicU32::new(NO_PENDING),
177            }),
178            depth: 0,
179            open: true,
180        })
181    }
182
183    /// Begins a nested transaction as a savepoint one level deeper.
184    ///
185    /// # Errors
186    ///
187    /// Returns [`Error::Turso`](crate::Error::Turso) when a pending
188    /// rollback or the `SAVEPOINT` statement fails.
189    async fn begin_nested(&self) -> Result<Self> {
190        self.shared.settle().await?;
191        let depth = self.shared.depth.load(Ordering::Acquire) + 1;
192        self.shared
193            .conn
194            .execute_raw(&format!("SAVEPOINT sp{depth}"))
195            .await?;
196        self.shared.depth.store(depth, Ordering::Release);
197        Ok(Self {
198            shared: Arc::clone(&self.shared),
199            depth,
200            open: true,
201        })
202    }
203
204    /// The savepoint depth: `0` for a top-level transaction.
205    pub fn depth(&self) -> u32 {
206        self.depth
207    }
208
209    /// The pinned engine connection.
210    fn conn(&self) -> &Conn {
211        &self.shared.conn
212    }
213
214    /// Finishes the transaction, rolling back when `rollback` is set and
215    /// committing otherwise.
216    ///
217    /// `open` is cleared first so that `Drop` stays inert even if the
218    /// engine statement fails; a failed commit leaves the connection in an
219    /// unknown state, which the pool detects through `is_autocommit`.
220    ///
221    /// # Errors
222    ///
223    /// Returns [`Error::Turso`](crate::Error::Turso) when a pending rollback
224    /// or the finishing statement fails.
225    async fn finish(mut self, rollback: bool) -> Result<()> {
226        self.open = false;
227        self.shared.settle().await?;
228        if self.depth == 0 {
229            self.conn()
230                .execute_raw(if rollback { "ROLLBACK" } else { "COMMIT" })
231                .await?;
232        } else if rollback {
233            rollback_to(self.conn(), self.depth).await?;
234            self.shared.depth.store(self.depth - 1, Ordering::Release);
235        } else {
236            self.conn()
237                .execute_raw(&format!("RELEASE SAVEPOINT sp{}", self.depth))
238                .await?;
239            self.shared.depth.store(self.depth - 1, Ordering::Release);
240        }
241        Ok(())
242    }
243
244    /// Commits the transaction — `COMMIT`, or `RELEASE SAVEPOINT` when
245    /// nested.
246    ///
247    /// # Errors
248    ///
249    /// Returns [`Error::Turso`](crate::Error::Turso) when the commit fails;
250    /// an MVCC write conflict surfaces here with
251    /// [`ErrorKind::Busy`](crate::ErrorKind::Busy).
252    pub async fn commit(self) -> Result<()> {
253        self.finish(false).await
254    }
255
256    /// Rolls the transaction back — `ROLLBACK`, or `ROLLBACK TO SAVEPOINT`
257    /// when nested.
258    ///
259    /// # Errors
260    ///
261    /// Returns [`Error::Turso`](crate::Error::Turso) when the rollback
262    /// fails.
263    pub async fn rollback(self) -> Result<()> {
264        self.finish(true).await
265    }
266}
267
268impl Drop for Transaction {
269    fn drop(&mut self) {
270        if !self.open {
271            return;
272        }
273        if self.depth == 0 {
274            // Nothing can be awaited here, so the connection is withheld
275            // from the pool; the engine rolls back when it is closed.
276            tracing::warn!("transaction dropped without commit or rollback; discarding connection");
277            self.shared.conn.discard();
278        } else {
279            // Record the shallowest dropped depth: rolling back to it also
280            // undoes every deeper savepoint, so a deeper pending depth is
281            // superseded and a shallower one is kept.
282            self.shared
283                .pending_rollback
284                .fetch_min(self.depth, Ordering::AcqRel);
285        }
286    }
287}
288
289#[async_trait]
290impl ConnectionTrait for Transaction {
291    async fn execute(&self, statement: Statement) -> Result<ExecResult> {
292        self.shared.settle().await?;
293        executor::execute(self.conn(), &statement).await
294    }
295
296    async fn execute_unprepared(&self, sql: &str) -> Result<ExecResult> {
297        self.shared.settle().await?;
298        executor::execute_unprepared(self.conn(), sql).await
299    }
300
301    async fn query_one(&self, statement: Statement) -> Result<Option<Row>> {
302        self.shared.settle().await?;
303        executor::query_one(self.conn(), &statement).await
304    }
305
306    async fn query_all(&self, statement: Statement) -> Result<Vec<Row>> {
307        self.shared.settle().await?;
308        executor::query_all(self.conn(), &statement).await
309    }
310}
311
312impl StreamTrait for Transaction {
313    fn stream<'a>(
314        &'a self,
315        statement: Statement,
316    ) -> Pin<Box<dyn Future<Output = Result<RowStream<'a>>> + Send + 'a>> {
317        Box::pin(async move {
318            self.shared.settle().await?;
319            // The stream borrows the transaction, which already pins the
320            // connection, so there is nothing extra to hold.
321            executor::stream(self.conn(), &statement, ()).await
322        })
323    }
324}
325
326#[async_trait]
327impl TransactionTrait for Transaction {
328    async fn begin(&self) -> Result<Transaction> {
329        self.begin_nested().await
330    }
331
332    async fn begin_with_mode(&self, _mode: TransactionMode) -> Result<Transaction> {
333        // Nested transactions are savepoints, which have no mode; the mode
334        // only applies at depth 0.
335        self.begin_nested().await
336    }
337}