Skip to main content

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}