Skip to main content

mssql_client/
transaction.rs

1//! Transaction support.
2//!
3//! This module provides transaction isolation levels, savepoint support,
4//! and transaction abstractions for SQL Server.
5
6/// Transaction isolation level.
7///
8/// SQL Server supports these isolation levels for transaction management.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10#[non_exhaustive]
11pub enum IsolationLevel {
12    /// Read uncommitted (dirty reads allowed).
13    ///
14    /// Lowest isolation - transactions can read uncommitted changes from
15    /// other transactions. Offers best performance but no consistency guarantees.
16    ReadUncommitted,
17
18    /// Read committed (default for SQL Server).
19    ///
20    /// Transactions can only read committed data. Prevents dirty reads
21    /// but allows non-repeatable reads and phantom reads.
22    #[default]
23    ReadCommitted,
24
25    /// Repeatable read.
26    ///
27    /// Ensures rows read by a transaction don't change during the transaction.
28    /// Prevents dirty reads and non-repeatable reads, but allows phantom reads.
29    RepeatableRead,
30
31    /// Serializable (highest isolation).
32    ///
33    /// Strictest isolation - transactions are completely isolated from
34    /// each other. Prevents all read phenomena but has highest lock contention.
35    Serializable,
36
37    /// Snapshot isolation.
38    ///
39    /// Uses row versioning to provide a point-in-time view of data.
40    /// Requires snapshot isolation to be enabled on the database.
41    Snapshot,
42}
43
44impl IsolationLevel {
45    /// Get the SQL statement to set this isolation level.
46    #[must_use]
47    pub fn as_sql(&self) -> &'static str {
48        match self {
49            Self::ReadUncommitted => "SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED",
50            Self::ReadCommitted => "SET TRANSACTION ISOLATION LEVEL READ COMMITTED",
51            Self::RepeatableRead => "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ",
52            Self::Serializable => "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE",
53            Self::Snapshot => "SET TRANSACTION ISOLATION LEVEL SNAPSHOT",
54        }
55    }
56
57    /// Get the isolation level name as used in SQL Server.
58    #[must_use]
59    pub fn name(&self) -> &'static str {
60        match self {
61            Self::ReadUncommitted => "READ UNCOMMITTED",
62            Self::ReadCommitted => "READ COMMITTED",
63            Self::RepeatableRead => "REPEATABLE READ",
64            Self::Serializable => "SERIALIZABLE",
65            Self::Snapshot => "SNAPSHOT",
66        }
67    }
68}
69
70/// A savepoint within a transaction.
71///
72/// Savepoints allow partial rollbacks within a transaction.
73/// The savepoint name is validated when created to prevent SQL injection.
74///
75/// # Example
76///
77/// ```rust,no_run
78/// # async fn ex(client: mssql_client::Client<mssql_client::Ready>) -> Result<(), mssql_client::Error> {
79/// let mut tx = client.begin_transaction().await?;
80///
81/// tx.execute("INSERT INTO orders (customer_id) VALUES (@p1)", &[&42]).await?;
82/// let sp = tx.save_point("before_items").await?;
83///
84/// tx.execute("INSERT INTO items (order_id, product_id) VALUES (@p1, @p2)", &[&1, &100]).await?;
85///
86/// // Oops, need to undo the items but keep the order
87/// tx.rollback_to(&sp).await?;
88///
89/// // Continue with different items...
90/// tx.commit().await?;
91/// # Ok(())
92/// # }
93/// ```
94#[derive(Debug, Clone)]
95#[must_use = "a savepoint should be used to rollback or it has no effect"]
96pub struct SavePoint {
97    /// The validated savepoint name.
98    pub(crate) name: String,
99}
100
101impl SavePoint {
102    /// Create a new savepoint with a validated name.
103    ///
104    /// This is called internally after name validation.
105    pub(crate) fn new(name: String) -> Self {
106        Self { name }
107    }
108
109    /// Get the savepoint name.
110    #[must_use]
111    pub fn name(&self) -> &str {
112        &self.name
113    }
114}
115
116/// A database transaction abstraction.
117///
118/// This is a higher-level transaction wrapper that can be used
119/// with closure-based APIs or as a standalone type.
120#[must_use = "a transaction must be committed or rolled back"]
121pub struct Transaction<'a> {
122    isolation_level: IsolationLevel,
123    _marker: std::marker::PhantomData<&'a ()>,
124}
125
126impl Transaction<'_> {
127    /// Create a new transaction with default isolation level.
128    #[allow(dead_code)] // Used when transaction begin is implemented
129    pub(crate) fn new() -> Self {
130        Self {
131            isolation_level: IsolationLevel::default(),
132            _marker: std::marker::PhantomData,
133        }
134    }
135
136    /// Create a new transaction with specified isolation level.
137    #[allow(dead_code)] // Used when transaction begin is implemented
138    pub(crate) fn with_isolation_level(level: IsolationLevel) -> Self {
139        Self {
140            isolation_level: level,
141            _marker: std::marker::PhantomData,
142        }
143    }
144
145    /// Get the isolation level of this transaction.
146    #[must_use]
147    pub fn isolation_level(&self) -> IsolationLevel {
148        self.isolation_level
149    }
150}
151
152#[cfg(test)]
153#[allow(clippy::unwrap_used)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_isolation_level_sql() {
159        assert_eq!(
160            IsolationLevel::ReadCommitted.as_sql(),
161            "SET TRANSACTION ISOLATION LEVEL READ COMMITTED"
162        );
163        assert_eq!(
164            IsolationLevel::Snapshot.as_sql(),
165            "SET TRANSACTION ISOLATION LEVEL SNAPSHOT"
166        );
167    }
168
169    #[test]
170    fn test_isolation_level_name() {
171        assert_eq!(IsolationLevel::ReadCommitted.name(), "READ COMMITTED");
172        assert_eq!(IsolationLevel::Serializable.name(), "SERIALIZABLE");
173    }
174
175    #[test]
176    fn test_savepoint_name() {
177        let sp = SavePoint::new("my_savepoint".to_string());
178        assert_eq!(sp.name(), "my_savepoint");
179        // SavePoint now has no lifetime parameter
180        assert_eq!(sp.name, "my_savepoint");
181    }
182
183    #[test]
184    fn test_default_isolation_level() {
185        let level = IsolationLevel::default();
186        assert_eq!(level, IsolationLevel::ReadCommitted);
187    }
188}