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,ignore
78/// let mut tx = client.begin_transaction().await?;
79///
80/// tx.execute("INSERT INTO orders (customer_id) VALUES (@p1)", &[&42]).await?;
81/// let sp = tx.save_point("before_items").await?;
82///
83/// tx.execute("INSERT INTO items (order_id, product_id) VALUES (@p1, @p2)", &[&1, &100]).await?;
84///
85/// // Oops, need to undo the items but keep the order
86/// tx.rollback_to(&sp).await?;
87///
88/// // Continue with different items...
89/// tx.commit().await?;
90/// ```
91#[derive(Debug, Clone)]
92#[must_use = "a savepoint should be used to rollback or it has no effect"]
93pub struct SavePoint {
94 /// The validated savepoint name.
95 pub(crate) name: String,
96}
97
98impl SavePoint {
99 /// Create a new savepoint with a validated name.
100 ///
101 /// This is called internally after name validation.
102 pub(crate) fn new(name: String) -> Self {
103 Self { name }
104 }
105
106 /// Get the savepoint name.
107 #[must_use]
108 pub fn name(&self) -> &str {
109 &self.name
110 }
111}
112
113/// A database transaction abstraction.
114///
115/// This is a higher-level transaction wrapper that can be used
116/// with closure-based APIs or as a standalone type.
117#[must_use = "a transaction must be committed or rolled back"]
118pub struct Transaction<'a> {
119 isolation_level: IsolationLevel,
120 _marker: std::marker::PhantomData<&'a ()>,
121}
122
123impl Transaction<'_> {
124 /// Create a new transaction with default isolation level.
125 #[allow(dead_code)] // Used when transaction begin is implemented
126 pub(crate) fn new() -> Self {
127 Self {
128 isolation_level: IsolationLevel::default(),
129 _marker: std::marker::PhantomData,
130 }
131 }
132
133 /// Create a new transaction with specified isolation level.
134 #[allow(dead_code)] // Used when transaction begin is implemented
135 pub(crate) fn with_isolation_level(level: IsolationLevel) -> Self {
136 Self {
137 isolation_level: level,
138 _marker: std::marker::PhantomData,
139 }
140 }
141
142 /// Get the isolation level of this transaction.
143 #[must_use]
144 pub fn isolation_level(&self) -> IsolationLevel {
145 self.isolation_level
146 }
147}
148
149#[cfg(test)]
150#[allow(clippy::unwrap_used)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_isolation_level_sql() {
156 assert_eq!(
157 IsolationLevel::ReadCommitted.as_sql(),
158 "SET TRANSACTION ISOLATION LEVEL READ COMMITTED"
159 );
160 assert_eq!(
161 IsolationLevel::Snapshot.as_sql(),
162 "SET TRANSACTION ISOLATION LEVEL SNAPSHOT"
163 );
164 }
165
166 #[test]
167 fn test_isolation_level_name() {
168 assert_eq!(IsolationLevel::ReadCommitted.name(), "READ COMMITTED");
169 assert_eq!(IsolationLevel::Serializable.name(), "SERIALIZABLE");
170 }
171
172 #[test]
173 fn test_savepoint_name() {
174 let sp = SavePoint::new("my_savepoint".to_string());
175 assert_eq!(sp.name(), "my_savepoint");
176 // SavePoint now has no lifetime parameter
177 assert_eq!(sp.name, "my_savepoint");
178 }
179
180 #[test]
181 fn test_default_isolation_level() {
182 let level = IsolationLevel::default();
183 assert_eq!(level, IsolationLevel::ReadCommitted);
184 }
185}