clt_database/turso/src/transaction.rs
1use std::{ops::Deref, sync::atomic::Ordering};
2
3use crate::turso::{Connection, Result, Statement};
4
5/// Options for transaction behavior. See [BEGIN
6/// TRANSACTION](http://www.sqlite.org/lang_transaction.html) for details.
7#[derive(Copy, Clone)]
8#[non_exhaustive]
9pub enum TransactionBehavior {
10 /// DEFERRED means that the transaction does not actually start until the
11 /// database is first accessed.
12 Deferred,
13 /// IMMEDIATE cause the database connection to start a new write
14 /// immediately, without waiting for a writes statement.
15 Immediate,
16 /// EXCLUSIVE prevents other database connections from reading the database
17 /// while the transaction is underway.
18 Exclusive,
19}
20
21/// Options for how a Transaction should behave when it is dropped.
22#[derive(Copy, Clone, Debug, PartialEq, Eq)]
23#[non_exhaustive]
24pub enum DropBehavior {
25 /// Roll back the changes. This is the default.
26 Rollback,
27
28 /// Commit the changes.
29 Commit,
30
31 /// Do not commit or roll back changes - this will leave the transaction or
32 /// savepoint open, so should be used with care.
33 Ignore,
34
35 /// Panic. Used to enforce intentional behavior during development.
36 Panic,
37}
38
39impl From<DropBehavior> for u8 {
40 fn from(behavior: DropBehavior) -> Self {
41 match behavior {
42 DropBehavior::Rollback => 0,
43 DropBehavior::Commit => 1,
44 DropBehavior::Ignore => 2,
45 DropBehavior::Panic => 3,
46 }
47 }
48}
49
50impl From<u8> for DropBehavior {
51 fn from(value: u8) -> Self {
52 match value {
53 0 => DropBehavior::Rollback,
54 1 => DropBehavior::Commit,
55 2 => DropBehavior::Ignore,
56 3 => DropBehavior::Panic,
57 _ => panic!("Invalid drop behavior: {value}"),
58 }
59 }
60}
61
62/// Represents a transaction on a database connection.
63///
64/// ## Note
65///
66/// Transactions will roll back by default. Use `commit` method to explicitly
67/// commit the transaction, or use `set_drop_behavior` to change what happens
68/// on the next access to the connection after the transaction is dropped.
69///
70/// ## Example
71///
72/// ```rust,no_run
73/// # use turso::{Connection, Result};
74/// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
75/// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
76/// async fn perform_queries(conn: &mut Connection) -> Result<()> {
77/// let tx = conn.transaction().await?;
78///
79/// do_queries_part_1(&tx)?; // tx causes rollback if this fails
80/// do_queries_part_2(&tx)?; // tx causes rollback if this fails
81///
82/// tx.commit().await
83/// }
84/// ```
85#[derive(Debug)]
86pub struct Transaction<'conn> {
87 conn: &'conn Connection,
88 drop_behavior: DropBehavior,
89 in_progress: bool,
90}
91
92impl Transaction<'_> {
93 /// Begin a new transaction. Cannot be nested;
94 ///
95 /// Even though we don't mutate the connection, we take a `&mut Connection`
96 /// to prevent nested transactions on the same connection. For cases
97 /// where this is unacceptable, [`Transaction::new_unchecked`] is available.
98 #[inline]
99 pub async fn new(
100 conn: &mut Connection,
101 behavior: TransactionBehavior,
102 ) -> Result<Transaction<'_>> {
103 Self::new_unchecked(conn, behavior).await
104 }
105
106 /// Begin a new transaction, failing if a transaction is open.
107 ///
108 /// If a transaction is already open, this will return an error. Where
109 /// possible, [`Transaction::new`] should be preferred, as it provides a
110 /// compile-time guarantee that transactions are not nested.
111 #[inline]
112 pub async fn new_unchecked(
113 conn: &Connection,
114 behavior: TransactionBehavior,
115 ) -> Result<Transaction<'_>> {
116 let query = match behavior {
117 TransactionBehavior::Deferred => "BEGIN DEFERRED",
118 TransactionBehavior::Immediate => "BEGIN IMMEDIATE",
119 TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE",
120 };
121 // TODO: Use execute_batch instead
122 conn.execute(query, ()).await.map(move |_| Transaction {
123 conn,
124 drop_behavior: DropBehavior::Rollback,
125 in_progress: true,
126 })
127 }
128
129 // Use the Connection to Prepare a statement.
130 // This allows a database update function to be passed a transaction,
131 // prepare a statement, and use it without needing direct access to the
132 // Connection
133 pub async fn prepare(&self, sql: &str) -> Result<Statement> {
134 self.conn.prepare(sql).await
135 }
136
137 /// Get the current setting for what happens to the transaction when it is
138 /// dropped.
139 #[inline]
140 #[must_use]
141 pub fn drop_behavior(&self) -> DropBehavior {
142 self.drop_behavior
143 }
144
145 /// Configure the transaction to perform the specified action when it is
146 /// dropped.
147 #[inline]
148 pub fn set_drop_behavior(&mut self, drop_behavior: DropBehavior) {
149 self.drop_behavior = drop_behavior;
150 }
151
152 /// A convenience method which consumes and commits a transaction.
153 #[inline]
154 pub async fn commit(mut self) -> Result<()> {
155 self._commit().await
156 }
157
158 #[inline]
159 async fn _commit(&mut self) -> Result<()> {
160 self.conn.execute("COMMIT", ()).await?;
161 self.in_progress = false;
162 Ok(())
163 }
164
165 /// A convenience method which consumes and rolls back a transaction.
166 #[inline]
167 pub async fn rollback(mut self) -> Result<()> {
168 self._rollback().await
169 }
170
171 #[inline]
172 async fn _rollback(&mut self) -> Result<()> {
173 self.conn.execute("ROLLBACK", ()).await?;
174 self.in_progress = false;
175 Ok(())
176 }
177
178 /// Consumes the transaction, committing or rolling back according to the
179 /// current setting (see `drop_behavior`).
180 ///
181 /// Functionally equivalent to the `Drop` implementation, but allows
182 /// callers to see any errors that occur.
183 #[inline]
184 pub async fn finish(mut self) -> Result<()> {
185 self._finish().await
186 }
187
188 #[inline]
189 async fn _finish(&mut self) -> Result<()> {
190 if self.conn.is_autocommit()? {
191 return Ok(());
192 }
193 match self.drop_behavior() {
194 DropBehavior::Commit => {
195 if (self._commit().await).is_err() {
196 self._rollback().await
197 } else {
198 Ok(())
199 }
200 }
201 DropBehavior::Rollback => self._rollback().await,
202 DropBehavior::Ignore => Ok(()),
203 DropBehavior::Panic => panic!("Transaction dropped unexpectedly."),
204 }
205 }
206}
207
208impl Deref for Transaction<'_> {
209 type Target = Connection;
210
211 #[inline]
212 fn deref(&self) -> &Connection {
213 self.conn
214 }
215}
216
217impl Drop for Transaction<'_> {
218 #[inline]
219 fn drop(&mut self) {
220 if self.in_progress {
221 self.conn
222 .dangling_tx
223 .store(self.drop_behavior(), Ordering::SeqCst);
224 } else {
225 self.conn
226 .dangling_tx
227 .store(DropBehavior::Ignore, Ordering::SeqCst);
228 }
229 }
230}
231
232impl Connection {
233 /// Begin a new transaction with the default behavior (DEFERRED).
234 ///
235 /// The transaction defaults to rolling back on the next access to the connection
236 /// if it is not finished when the transaction is dropped. If you
237 /// want the transaction to commit, you must call
238 /// [`commit`](Transaction::commit) or
239 /// [`set_drop_behavior(DropBehavior::Commit)`](Transaction::set_drop_behavior).
240 ///
241 /// ## Example
242 ///
243 /// ```rust,no_run
244 /// # use turso::{Connection, Result};
245 /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
246 /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
247 /// async fn perform_queries(conn: &mut Connection) -> Result<()> {
248 /// let tx = conn.transaction().await?;
249 ///
250 /// do_queries_part_1(&tx)?; // tx causes rollback if this fails
251 /// do_queries_part_2(&tx)?; // tx causes rollback if this fails
252 ///
253 /// tx.commit().await
254 /// }
255 /// ```
256 ///
257 /// # Failure
258 ///
259 /// Will return `Err` if the call fails.
260 #[inline]
261 pub async fn transaction(&mut self) -> Result<Transaction<'_>> {
262 self.transaction_with_behavior(self.transaction_behavior)
263 .await
264 }
265
266 /// Begin a new transaction with a specified behavior.
267 ///
268 /// See [`transaction`](Connection::transaction).
269 ///
270 /// # Failure
271 ///
272 /// Will return `Err` if the call fails.
273 #[inline]
274 pub async fn transaction_with_behavior(
275 &mut self,
276 behavior: TransactionBehavior,
277 ) -> Result<Transaction<'_>> {
278 self.maybe_handle_dangling_tx().await?;
279 Transaction::new(self, behavior).await
280 }
281
282 /// Begin a new transaction with the default behavior (DEFERRED).
283 ///
284 /// Attempt to open a nested transaction will result in a SQLite error.
285 /// `Connection::transaction` prevents this at compile time by taking `&mut
286 /// self`, but `Connection::unchecked_transaction()` may be used to defer
287 /// the checking until runtime.
288 ///
289 /// See [`Connection::transaction`] and [`Transaction::new_unchecked`]
290 /// (which can be used if the default transaction behavior is undesirable).
291 ///
292 /// ## Example
293 ///
294 /// ```rust,no_run
295 /// # use turso::{Connection, Result};
296 /// # use std::rc::Rc;
297 /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
298 /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
299 /// async fn perform_queries(conn: Rc<Connection>) -> Result<()> {
300 /// let tx = conn.unchecked_transaction().await?;
301 ///
302 /// do_queries_part_1(&tx)?; // tx causes rollback if this fails
303 /// do_queries_part_2(&tx)?; // tx causes rollback if this fails
304 ///
305 /// tx.commit().await
306 /// }
307 /// ```
308 ///
309 /// # Failure
310 ///
311 /// Will return `Err` if the underlying SQLite call fails. The specific
312 /// error returned if transactions are nested is currently unspecified.
313 pub async fn unchecked_transaction(&self) -> Result<Transaction<'_>> {
314 Transaction::new_unchecked(self, self.transaction_behavior).await
315 }
316
317 /// Set the default transaction behavior for the connection.
318 ///
319 /// ## Note
320 ///
321 /// This will only apply to transactions initiated by [`transaction`](Connection::transaction)
322 /// or [`unchecked_transaction`](Connection::unchecked_transaction).
323 ///
324 /// ## Example
325 ///
326 /// ```rust,no_run
327 /// # use turso::{Connection, Result};
328 /// # use turso::transaction::TransactionBehavior;
329 /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) }
330 /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) }
331 /// async fn perform_queries(conn: &mut Connection) -> Result<()> {
332 /// conn.set_transaction_behavior(TransactionBehavior::Immediate);
333 ///
334 /// let tx = conn.transaction().await?;
335 ///
336 /// do_queries_part_1(&tx)?; // tx causes rollback if this fails
337 /// do_queries_part_2(&tx)?; // tx causes rollback if this fails
338 ///
339 /// tx.commit().await
340 /// }
341 /// ```
342 pub fn set_transaction_behavior(&mut self, behavior: TransactionBehavior) {
343 self.transaction_behavior = behavior;
344 }
345}
346
347#[cfg(clt_turso_tests)]
348mod test {
349 use crate::turso::{Builder, Connection, Error, Result};
350
351 use super::DropBehavior;
352
353 async fn checked_memory_handle() -> Result<Connection> {
354 let db = Builder::new_local(":memory:").build().await?;
355 let conn = db.connect()?;
356 conn.execute("CREATE TABLE foo (x INTEGER)", ()).await?;
357 Ok(conn)
358 }
359
360 #[tokio::test]
361 async fn test_drop_rollback_on_new_transaction() {
362 let mut conn = checked_memory_handle().await.unwrap();
363 {
364 let tx = conn.transaction().await.unwrap();
365 tx.execute("INSERT INTO foo VALUES(?)", &[1]).await.unwrap();
366 // Drop without finish - should be rolled back when next transaction starts
367 }
368
369 // Start a new transaction - this should rollback the dangling one
370 let tx = conn.transaction().await.unwrap();
371 tx.execute("INSERT INTO foo VALUES(?)", &[2]).await.unwrap();
372 let result = tx
373 .prepare("SELECT SUM(x) FROM foo")
374 .await
375 .unwrap()
376 .query_row(())
377 .await
378 .unwrap();
379
380 // The insert from the dropped transaction should have been rolled back
381 assert_eq!(2, result.get::<i32>(0).unwrap());
382 tx.finish().await.unwrap();
383 }
384
385 #[tokio::test]
386 async fn test_drop_rollback_on_query() {
387 let mut conn = checked_memory_handle().await.unwrap();
388 {
389 let tx = conn.transaction().await.unwrap();
390 tx.execute("INSERT INTO foo VALUES(?)", &[1]).await.unwrap();
391 // Drop without finish - should be rolled back when conn.query is called
392 }
393
394 // Using conn.query should rollback the dangling transaction
395 let mut rows = conn.query("SELECT count(*) FROM foo", ()).await.unwrap();
396 let result = rows.next().await.unwrap().unwrap();
397
398 // The insert from the dropped transaction should have been rolled back
399 assert_eq!(0, result.get::<i32>(0).unwrap());
400 }
401
402 #[tokio::test]
403 async fn test_drop_rollback_on_execute() {
404 let mut conn = checked_memory_handle().await.unwrap();
405 {
406 let tx = conn.transaction().await.unwrap();
407 tx.execute("INSERT INTO foo VALUES(?)", &[1]).await.unwrap();
408 // Drop without finish - should be rolled back when conn.execute is called
409 }
410
411 // Using conn.execute should rollback the dangling transaction
412 conn.execute("INSERT INTO foo VALUES(?)", &[2])
413 .await
414 .unwrap();
415
416 let mut rows = conn.query("SELECT count(*) FROM foo", ()).await.unwrap();
417 let result = rows.next().await.unwrap().unwrap();
418
419 // The insert from the dropped transaction should have been rolled back
420 assert_eq!(1, result.get::<i32>(0).unwrap());
421 }
422
423 #[tokio::test]
424 async fn test_drop() -> Result<()> {
425 let _ = tracing_subscriber::fmt::try_init();
426 let mut conn = checked_memory_handle().await?;
427 {
428 let tx = conn.transaction().await?;
429 tx.execute("INSERT INTO foo VALUES(?)", &[1]).await?;
430 // default: rollback
431 }
432 {
433 let mut tx = conn.transaction().await?;
434 tx.execute("INSERT INTO foo VALUES(?)", &[2]).await?;
435 tx.set_drop_behavior(DropBehavior::Commit);
436 }
437 {
438 let tx = conn.transaction().await?;
439 let result = tx
440 .prepare("SELECT SUM(x) FROM foo")
441 .await?
442 .query_row(())
443 .await?;
444
445 assert_eq!(2, result.get::<i32>(0)?);
446 }
447 Ok(())
448 }
449
450 fn assert_nested_tx_error(e: Error) {
451 if let Error::Error(e) = &e {
452 assert!(e.contains("transaction"));
453 } else {
454 panic!("Unexpected error type: {e:?}");
455 }
456 }
457
458 #[tokio::test]
459 async fn test_unchecked_nesting() -> Result<()> {
460 let conn = checked_memory_handle().await?;
461
462 {
463 let tx = conn.unchecked_transaction().await?;
464 let e = tx.unchecked_transaction().await.unwrap_err();
465 assert_nested_tx_error(e);
466 tx.finish().await?;
467 // default: rollback
468 }
469 {
470 let tx = conn.unchecked_transaction().await?;
471 tx.execute("INSERT INTO foo VALUES(?)", &[1]).await?;
472 // Ensure this doesn't interfere with ongoing transaction
473 let e = tx.unchecked_transaction().await.unwrap_err();
474 assert_nested_tx_error(e);
475
476 tx.execute("INSERT INTO foo VALUES(?)", &[1]).await?;
477 tx.commit().await?;
478 }
479
480 let result = conn
481 .prepare("SELECT SUM(x) FROM foo")
482 .await?
483 .query_row(())
484 .await?;
485 assert_eq!(2, result.get::<i32>(0)?);
486 Ok(())
487 }
488
489 #[tokio::test]
490 async fn test_explicit_rollback_commit() -> Result<()> {
491 let mut conn = checked_memory_handle().await?;
492 {
493 let tx = conn.transaction().await?;
494 tx.execute("INSERT INTO foo VALUES(?)", &[1]).await?;
495 tx.rollback().await?;
496
497 // This is a current Turso's limitation.
498 // Since we don't have support for savepoints yet,
499 // a rollback ends with a transaction so we need to immediately open a new one.
500 let tx = conn.transaction().await?;
501 tx.execute("INSERT INTO foo VALUES(?)", &[2]).await?;
502 tx.commit().await?;
503 }
504 {
505 let tx = conn.transaction().await?;
506 tx.execute("INSERT INTO foo VALUES(?)", &[4]).await?;
507 tx.commit().await?;
508 }
509 {
510 let result = conn
511 .prepare("SELECT SUM(x) FROM foo")
512 .await?
513 .query_row(())
514 .await?;
515 assert_eq!(6, result.get::<i32>(0)?);
516 }
517 Ok(())
518 }
519}