Skip to main content

ferrule_sql/
transaction.rs

1//! Backend-aware transaction-control helpers.
2//!
3//! Lifted from `copy.rs` so that callers outside the copy module
4//! (notably the CLI `query` command's `--begin`/`--commit`/`--rollback`
5//! wiring) can drive the same BEGIN / COMMIT / ROLLBACK statements
6//! without depending on copy.rs internals.
7//!
8//! The string-statement chosen per backend mirrors what `copy.rs`
9//! already issues; Oracle has no explicit BEGIN (implicit txn), so
10//! [`begin_transaction`] is a noop that still returns `true` so the
11//! caller's wrapping COMMIT/ROLLBACK at the end terminates the
12//! implicit transaction.
13
14use crate::backend::Backend;
15use crate::connection::Connection;
16use crate::error::SqlError;
17
18/// SQL string the [`begin_transaction`] dispatcher will send for a
19/// given backend. `None` means "skip the wire round-trip" — Oracle
20/// uses this because issuing an explicit `BEGIN` would parse as a
21/// PL/SQL block.
22fn begin_sql_for(backend: Backend) -> Option<&'static str> {
23    match backend {
24        #[cfg(feature = "mssql")]
25        Backend::MsSql => Some("BEGIN TRANSACTION"),
26        #[cfg(feature = "oracle")]
27        Backend::Oracle => None,
28        _ => Some("BEGIN"),
29    }
30}
31
32/// SQL string for the [`commit_transaction`] dispatcher.
33fn commit_sql_for(backend: Backend) -> &'static str {
34    match backend {
35        #[cfg(feature = "mssql")]
36        Backend::MsSql => "COMMIT TRANSACTION",
37        _ => "COMMIT",
38    }
39}
40
41/// SQL string for the [`rollback_transaction`] dispatcher.
42fn rollback_sql_for(backend: Backend) -> &'static str {
43    match backend {
44        #[cfg(feature = "mssql")]
45        Backend::MsSql => "ROLLBACK TRANSACTION",
46        _ => "ROLLBACK",
47    }
48}
49
50/// Open a target-side transaction. Returns `true` if the BEGIN
51/// succeeded, `false` if the backend rejected the statement (best-
52/// effort: the caller proceeds without a wrapping transaction).
53///
54/// **Blocking:** issues at most one synchronous `BEGIN` round-trip.
55#[must_use]
56pub fn begin_transaction(conn: &mut dyn Connection, backend: Backend) -> bool {
57    match begin_sql_for(backend) {
58        None => true,
59        Some(stmt) => conn.execute(stmt).is_ok(),
60    }
61}
62
63/// Commit the wrapping transaction. **Blocking:** one synchronous
64/// `COMMIT` round-trip.
65#[must_use = "commit_transaction returns a SqlError on wire failure that the caller must surface"]
66pub fn commit_transaction(conn: &mut dyn Connection, backend: Backend) -> Result<(), SqlError> {
67    conn.execute(commit_sql_for(backend)).map(|_| ())
68}
69
70/// Roll back the wrapping transaction. **Blocking:** one synchronous
71/// `ROLLBACK` round-trip.
72#[must_use = "rollback_transaction returns a SqlError on wire failure; best-effort callers should still `let _ =`"]
73pub fn rollback_transaction(conn: &mut dyn Connection, backend: Backend) -> Result<(), SqlError> {
74    conn.execute(rollback_sql_for(backend)).map(|_| ())
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn statements_per_backend() {
83        // Default backends: BEGIN / COMMIT / ROLLBACK.
84        #[cfg(feature = "postgres")]
85        {
86            let b = Backend::Postgres;
87            assert_eq!(begin_sql_for(b), Some("BEGIN"));
88            assert_eq!(commit_sql_for(b), "COMMIT");
89            assert_eq!(rollback_sql_for(b), "ROLLBACK");
90        }
91        #[cfg(feature = "mysql")]
92        {
93            let b = Backend::MySql;
94            assert_eq!(begin_sql_for(b), Some("BEGIN"));
95            assert_eq!(commit_sql_for(b), "COMMIT");
96            assert_eq!(rollback_sql_for(b), "ROLLBACK");
97        }
98        #[cfg(feature = "sqlite")]
99        {
100            let b = Backend::Sqlite;
101            assert_eq!(begin_sql_for(b), Some("BEGIN"));
102            assert_eq!(commit_sql_for(b), "COMMIT");
103            assert_eq!(rollback_sql_for(b), "ROLLBACK");
104        }
105
106        // MSSQL uses the "TRANSACTION" suffix.
107        #[cfg(feature = "mssql")]
108        {
109            assert_eq!(begin_sql_for(Backend::MsSql), Some("BEGIN TRANSACTION"));
110            assert_eq!(commit_sql_for(Backend::MsSql), "COMMIT TRANSACTION");
111            assert_eq!(rollback_sql_for(Backend::MsSql), "ROLLBACK TRANSACTION");
112        }
113
114        // Oracle: BEGIN is a noop (None); COMMIT/ROLLBACK still send.
115        #[cfg(feature = "oracle")]
116        {
117            assert_eq!(begin_sql_for(Backend::Oracle), None);
118            assert_eq!(commit_sql_for(Backend::Oracle), "COMMIT");
119            assert_eq!(rollback_sql_for(Backend::Oracle), "ROLLBACK");
120        }
121    }
122}