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}