tdsql/transaction.rs
1//! Transactions and savepoints.
2
3use crate::client::Client;
4use crate::command::Command;
5use crate::dataset::DataSet;
6use crate::error::{Error, Result};
7use crate::row::Row;
8use crate::stream::RowStream;
9use crate::value::{FromSql, ToSql};
10
11/// The isolation level a transaction runs at.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13#[non_exhaustive]
14pub enum IsolationLevel {
15 /// The connection's current level: `READ COMMITTED`, unless
16 /// `SET TRANSACTION ISOLATION LEVEL` changed it.
17 #[default]
18 Unspecified,
19 /// `READ UNCOMMITTED` — dirty reads allowed.
20 ReadUncommitted,
21 /// `READ COMMITTED` — SQL Server's own default.
22 ReadCommitted,
23 /// `REPEATABLE READ`.
24 RepeatableRead,
25 /// `SERIALIZABLE`.
26 Serializable,
27 /// `SNAPSHOT` — requires the database to have snapshot isolation enabled.
28 Snapshot,
29}
30
31impl From<IsolationLevel> for tds_driver::IsolationLevel {
32 fn from(level: IsolationLevel) -> Self {
33 match level {
34 IsolationLevel::Unspecified => tds_driver::IsolationLevel::Unspecified,
35 IsolationLevel::ReadUncommitted => tds_driver::IsolationLevel::ReadUncommitted,
36 IsolationLevel::ReadCommitted => tds_driver::IsolationLevel::ReadCommitted,
37 IsolationLevel::RepeatableRead => tds_driver::IsolationLevel::RepeatableRead,
38 IsolationLevel::Serializable => tds_driver::IsolationLevel::Serializable,
39 IsolationLevel::Snapshot => tds_driver::IsolationLevel::Snapshot,
40 }
41 }
42}
43
44/// Whether this handle owns the transaction itself or just a savepoint in it.
45#[derive(Debug, Clone, PartialEq, Eq)]
46enum Scope {
47 Root,
48 Savepoint(String),
49}
50
51/// An in-progress transaction.
52///
53/// Created by [`Client::transaction`]. Every statement run through it is
54/// scoped to the transaction. It borrows the client mutably, so the connection
55/// cannot be used behind the transaction's back — which is honest, since TDS
56/// carries one in-flight request per connection anyway.
57///
58/// Call [`commit`](Self::commit) to keep the work or [`rollback`](Self::rollback)
59/// to discard it. **Dropping without doing either discards the work** — a
60/// transaction never commits implicitly.
61///
62/// The server can end a transaction by itself: a deadlock, an error under
63/// `SET XACT_ABORT ON`, a `ROLLBACK` in a procedure. The handle notices, and
64/// from then on every statement fails with [`Error::TransactionAborted`]
65/// rather than running outside any transaction; `commit` fails the same way,
66/// and `rollback` succeeds, there being nothing left to undo.
67///
68/// ```no_run
69/// # use tdsql::Client;
70/// # async fn f(client: &mut Client) -> tdsql::Result<()> {
71/// let mut tx = client.transaction().await?;
72///
73/// tx.execute("INSERT INTO orders (id) VALUES (@P1)", &[&1i32]).await?;
74/// tx.execute("UPDATE stock SET qty = qty - 1 WHERE id = @P1", &[&7i32]).await?;
75///
76/// tx.commit().await?;
77/// # Ok(())
78/// # }
79/// ```
80///
81/// A `&mut Transaction` can be handed to helper functions, so the unit of work
82/// can be assembled across several of them:
83///
84/// ```no_run
85/// # use tdsql::{Client, Transaction};
86/// async fn add_order(tx: &mut Transaction<'_>, id: i32) -> tdsql::Result<()> {
87/// tx.execute("INSERT INTO orders (id) VALUES (@P1)", &[&id]).await?;
88/// Ok(())
89/// }
90///
91/// # async fn f(client: &mut Client) -> tdsql::Result<()> {
92/// let mut tx = client.transaction().await?;
93/// add_order(&mut tx, 1).await?;
94/// add_order(&mut tx, 2).await?;
95/// tx.commit().await?;
96/// # Ok(())
97/// # }
98/// ```
99///
100/// # Rollback on drop
101///
102/// Dropping without committing rolls back. `Drop` cannot run `async` code, so
103/// the rollback is *enqueued* on the connection task rather than awaited — the
104/// send itself is synchronous and non-blocking. Requests are served in order,
105/// so the rollback reaches the server before any later statement on this
106/// connection. Use [`rollback`](Self::rollback) instead when you want to
107/// observe an error from it.
108#[derive(Debug)]
109pub struct Transaction<'a> {
110 client: &'a mut Client,
111 scope: Scope,
112 done: bool,
113}
114
115impl<'a> Transaction<'a> {
116 pub(crate) async fn begin(client: &'a mut Client, level: IsolationLevel) -> Result<Self> {
117 client.begin_transaction(level).await?;
118 Ok(Self {
119 client,
120 scope: Scope::Root,
121 done: false,
122 })
123 }
124
125 /// Commit the work.
126 ///
127 /// For a savepoint this keeps the work and folds it into the surrounding
128 /// transaction; SQL Server has no `RELEASE SAVEPOINT`, so nothing is sent.
129 pub async fn commit(mut self) -> Result<()> {
130 self.done = true;
131 match &self.scope {
132 Scope::Root => self.client.commit_transaction().await,
133 Scope::Savepoint(_) => Ok(()),
134 }
135 }
136
137 /// Discard the work.
138 ///
139 /// Equivalent to dropping the transaction, but reports any error instead of
140 /// swallowing it, and takes effect immediately rather than before the next
141 /// statement.
142 pub async fn rollback(mut self) -> Result<()> {
143 self.done = true;
144 match &self.scope {
145 Scope::Root => self.client.rollback_transaction().await,
146 Scope::Savepoint(name) => {
147 let name = name.clone();
148 self.client.rollback_to_savepoint(&name).await
149 }
150 }
151 }
152
153 /// The connection underneath, for the `Executor` default method bodies.
154 pub(crate) fn client_mut(&mut self) -> &mut Client {
155 self.client
156 }
157
158 /// Open a nested transaction, backed by an automatically named savepoint.
159 pub async fn transaction(&mut self) -> Result<Transaction<'_>> {
160 let name = format!("_tdsql_sp{}", self.client.next_savepoint_id());
161 self.savepoint(name).await
162 }
163
164 /// Open a nested transaction at a named savepoint.
165 ///
166 /// The name must be a plain SQL identifier — letters, digits and
167 /// underscores, starting with a letter or underscore, at most 32
168 /// characters, which is SQL Server's limit for savepoint names.
169 pub async fn savepoint(&mut self, name: impl Into<String>) -> Result<Transaction<'_>> {
170 let name = name.into();
171 validate_savepoint_name(&name)?;
172 self.client.save_transaction(&name).await?;
173 Ok(Transaction {
174 client: &mut *self.client,
175 scope: Scope::Savepoint(name),
176 done: false,
177 })
178 }
179
180 /// Run a query and collect every row of the first result set.
181 pub async fn query(&mut self, sql: &str, params: &[&dyn ToSql]) -> Result<Vec<Row>> {
182 self.client.query(sql, params).await
183 }
184
185 /// Run a query and receive the rows of its first result set as they
186 /// arrive. See [`Client::query_stream`].
187 pub async fn query_stream(
188 &mut self,
189 sql: &str,
190 params: &[&dyn ToSql],
191 ) -> Result<RowStream<'_>> {
192 self.client.query_stream(sql, params).await
193 }
194
195 /// Run a query expecting exactly one row.
196 pub async fn query_one(&mut self, sql: &str, params: &[&dyn ToSql]) -> Result<Row> {
197 self.client.query_one(sql, params).await
198 }
199
200 /// Run a query expecting at most one row.
201 pub async fn query_opt(&mut self, sql: &str, params: &[&dyn ToSql]) -> Result<Option<Row>> {
202 self.client.query_opt(sql, params).await
203 }
204
205 /// Run a query and read the first column of the first row.
206 pub async fn query_scalar<T: FromSql>(
207 &mut self,
208 sql: &str,
209 params: &[&dyn ToSql],
210 ) -> Result<T> {
211 self.client.query_scalar(sql, params).await
212 }
213
214 /// Run a statement and report how many rows it affected.
215 pub async fn execute(&mut self, sql: &str, params: &[&dyn ToSql]) -> Result<u64> {
216 self.client.execute(sql, params).await
217 }
218
219 /// Run a SQL batch verbatim and collect its rows.
220 pub async fn batch(&mut self, sql: &str) -> Result<Vec<Row>> {
221 self.client.batch(sql).await
222 }
223
224 /// Run a SQL batch verbatim and collect every result set.
225 pub async fn batch_dataset(&mut self, sql: &str) -> Result<DataSet> {
226 self.client.batch_dataset(sql).await
227 }
228
229 /// Run a [`Command`] and collect every result set it produces.
230 pub async fn query_dataset(&mut self, command: &Command) -> Result<DataSet> {
231 self.client.query_dataset(command).await
232 }
233
234 /// Run a [`Command`] for its row count rather than its rows.
235 pub async fn execute_command(&mut self, command: &Command) -> Result<u64> {
236 self.client.execute_command(command).await
237 }
238}
239
240impl Drop for Transaction<'_> {
241 fn drop(&mut self) {
242 if self.done {
243 return;
244 }
245 // `Drop` cannot await, but it can *enqueue*: the channel send is
246 // synchronous and non-blocking, and the connection task serves
247 // requests in order, so the rollback is guaranteed to reach the server
248 // before any later statement.
249 match &self.scope {
250 Scope::Root => self.client.enqueue_rollback(),
251 Scope::Savepoint(name) => self.client.enqueue_rollback_to(name.clone()),
252 }
253 }
254}
255
256/// SQL Server savepoint names are identifiers, and they are interpolated into
257/// `ROLLBACK TRANSACTION <name>`, so they must be checked rather than trusted.
258fn validate_savepoint_name(name: &str) -> Result<()> {
259 let valid = !name.is_empty()
260 && name.len() <= 32
261 && name
262 .chars()
263 .next()
264 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
265 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
266
267 if valid {
268 Ok(())
269 } else {
270 Err(Error::InvalidSavepointName(name.to_string()))
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn accepts_plain_identifiers() {
280 assert!(validate_savepoint_name("sp1").is_ok());
281 assert!(validate_savepoint_name("_tdsql_sp0").is_ok());
282 assert!(validate_savepoint_name("Before_Risky_Bit").is_ok());
283 }
284
285 #[test]
286 fn rejects_injection_and_malformed_names() {
287 for bad in [
288 "",
289 "1sp",
290 "sp name",
291 "sp;DROP TABLE users",
292 "sp'--",
293 "sp-1",
294 "spécial",
295 &"x".repeat(33),
296 ] {
297 assert!(
298 validate_savepoint_name(bad).is_err(),
299 "should have rejected {bad:?}"
300 );
301 }
302 }
303
304 #[test]
305 fn rejects_at_the_length_boundary() {
306 assert!(validate_savepoint_name(&"x".repeat(32)).is_ok());
307 assert!(validate_savepoint_name(&"x".repeat(33)).is_err());
308 }
309
310 #[test]
311 fn maps_isolation_levels() {
312 use tds_driver::IsolationLevel as T;
313 assert_eq!(T::from(IsolationLevel::Unspecified), T::Unspecified);
314 assert_eq!(T::from(IsolationLevel::ReadCommitted), T::ReadCommitted);
315 assert_eq!(T::from(IsolationLevel::Serializable), T::Serializable);
316 assert_eq!(T::from(IsolationLevel::Snapshot), T::Snapshot);
317 }
318
319 #[test]
320 fn defaults_to_unspecified() {
321 assert_eq!(IsolationLevel::default(), IsolationLevel::Unspecified);
322 }
323}