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