Skip to main content

tiberius/tds/codec/
transaction_manager.rs

1use super::{AllHeaderTy, Encode, ALL_HEADERS_LEN_TX};
2use bytes::{BufMut, BytesMut};
3use std::borrow::Cow;
4
5uint_enum! {
6    /// The request type of a Transaction Manager request, as defined in
7    /// MS-TDS 2.2.6.8 (`TM_*` request kinds).
8    #[repr(u16)]
9    pub enum TransactionManagerRequestType {
10        /// Get the address of the Distributed Transaction Coordinator.
11        GetDtcAddress = 0,
12        /// Import an existing distributed transaction (propagate).
13        Propagate = 1,
14        /// Begin a new transaction (`TM_BEGIN_XACT`).
15        Begin = 5,
16        /// Promote a local transaction to a distributed one (`TM_PROMOTE_XACT`).
17        Promote = 6,
18        /// Commit the active transaction (`TM_COMMIT_XACT`).
19        Commit = 7,
20        /// Roll back the active transaction (`TM_ROLLBACK_XACT`).
21        Rollback = 8,
22        /// Create a savepoint in the active transaction (`TM_SAVE_XACT`).
23        Save = 9,
24    }
25}
26
27uint_enum! {
28    /// The transaction isolation level requested when beginning a transaction
29    /// through a Transaction Manager request (MS-TDS 2.2.6.8).
30    #[repr(u8)]
31    pub enum IsolationLevel {
32        /// Use the server's default isolation level.
33        Unspecified = 0x00,
34        /// `READ UNCOMMITTED`.
35        ReadUncommitted = 0x01,
36        /// `READ COMMITTED`.
37        ReadCommitted = 0x02,
38        /// `REPEATABLE READ`.
39        RepeatableRead = 0x03,
40        /// `SERIALIZABLE`.
41        Serializable = 0x04,
42        /// `SNAPSHOT`.
43        Snapshot = 0x05,
44    }
45}
46
47/// A Transaction Manager request (packet type `0x14`, MS-TDS 2.2.6.8).
48///
49/// These requests let the client begin, commit, roll back or create a
50/// savepoint in a transaction directly through the TDS protocol instead of
51/// issuing the equivalent T-SQL batch (`BEGIN TRAN`, `COMMIT`, ...).
52///
53/// Every request carries the current transaction descriptor in the request's
54/// `ALL_HEADERS` block so the server can associate it with the correct
55/// transaction.
56#[derive(Debug, Clone)]
57pub struct TransactionManagerRequest<'a> {
58    transaction_desc: [u8; 8],
59    body: TransactionRequestBody<'a>,
60}
61
62#[derive(Debug, Clone)]
63enum TransactionRequestBody<'a> {
64    Begin {
65        isolation_level: IsolationLevel,
66        name: Cow<'a, str>,
67    },
68    Commit {
69        name: Cow<'a, str>,
70    },
71    Rollback {
72        name: Cow<'a, str>,
73    },
74    Save {
75        name: Cow<'a, str>,
76    },
77}
78
79impl<'a> TransactionManagerRequest<'a> {
80    /// Build a `TM_BEGIN_XACT` request that begins a new transaction with the
81    /// given isolation level. The (usually empty) transaction name is sent as
82    /// a `B_VARCHAR`.
83    pub fn begin(
84        transaction_desc: [u8; 8],
85        isolation_level: IsolationLevel,
86        name: impl Into<Cow<'a, str>>,
87    ) -> Self {
88        Self {
89            transaction_desc,
90            body: TransactionRequestBody::Begin {
91                isolation_level,
92                name: name.into(),
93            },
94        }
95    }
96
97    /// Build a `TM_COMMIT_XACT` request that commits the active transaction.
98    pub fn commit(transaction_desc: [u8; 8], name: impl Into<Cow<'a, str>>) -> Self {
99        Self {
100            transaction_desc,
101            body: TransactionRequestBody::Commit { name: name.into() },
102        }
103    }
104
105    /// Build a `TM_ROLLBACK_XACT` request that rolls back the active
106    /// transaction (or to a savepoint of the given name).
107    pub fn rollback(transaction_desc: [u8; 8], name: impl Into<Cow<'a, str>>) -> Self {
108        Self {
109            transaction_desc,
110            body: TransactionRequestBody::Rollback { name: name.into() },
111        }
112    }
113
114    /// Build a `TM_SAVE_XACT` request that creates a savepoint with the given
115    /// name in the active transaction.
116    pub fn save(transaction_desc: [u8; 8], name: impl Into<Cow<'a, str>>) -> Self {
117        Self {
118            transaction_desc,
119            body: TransactionRequestBody::Save { name: name.into() },
120        }
121    }
122
123    fn request_type(&self) -> TransactionManagerRequestType {
124        match self.body {
125            TransactionRequestBody::Begin { .. } => TransactionManagerRequestType::Begin,
126            TransactionRequestBody::Commit { .. } => TransactionManagerRequestType::Commit,
127            TransactionRequestBody::Rollback { .. } => TransactionManagerRequestType::Rollback,
128            TransactionRequestBody::Save { .. } => TransactionManagerRequestType::Save,
129        }
130    }
131}
132
133/// Encodes a `B_VARCHAR`: a single-byte count of UTF-16 code units followed by
134/// the string encoded as little-endian UCS-2 (MS-TDS 2.2.5.1.2).
135fn encode_b_varchar(dst: &mut BytesMut, s: &str) {
136    let units: Vec<u16> = s.encode_utf16().collect();
137    dst.put_u8(units.len() as u8);
138
139    for unit in units {
140        dst.put_u16_le(unit);
141    }
142}
143
144impl<'a> Encode<BytesMut> for TransactionManagerRequest<'a> {
145    fn encode(self, dst: &mut BytesMut) -> crate::Result<()> {
146        // ALL_HEADERS block carrying the transaction descriptor.
147        dst.put_u32_le(ALL_HEADERS_LEN_TX as u32);
148        dst.put_u32_le(ALL_HEADERS_LEN_TX as u32 - 4);
149        dst.put_u16_le(AllHeaderTy::TransactionDescriptor as u16);
150        dst.put_slice(&self.transaction_desc);
151        dst.put_u32_le(1);
152
153        // Request type (USHORT).
154        dst.put_u16_le(self.request_type() as u16);
155
156        match self.body {
157            TransactionRequestBody::Begin {
158                isolation_level,
159                name,
160            } => {
161                dst.put_u8(isolation_level as u8);
162                encode_b_varchar(dst, &name);
163            }
164            TransactionRequestBody::Commit { name } | TransactionRequestBody::Rollback { name } => {
165                encode_b_varchar(dst, &name);
166                // Flags byte: bit 0 (`fBeginXact`) unset — do not begin a new
167                // transaction after commit/rollback.
168                dst.put_u8(0);
169            }
170            TransactionRequestBody::Save { name } => {
171                encode_b_varchar(dst, &name);
172            }
173        }
174
175        Ok(())
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    fn all_headers() -> Vec<u8> {
184        let mut v = Vec::new();
185        v.extend_from_slice(&(ALL_HEADERS_LEN_TX as u32).to_le_bytes());
186        v.extend_from_slice(&(ALL_HEADERS_LEN_TX as u32 - 4).to_le_bytes());
187        v.extend_from_slice(&(AllHeaderTy::TransactionDescriptor as u16).to_le_bytes());
188        v.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
189        v.extend_from_slice(&1u32.to_le_bytes());
190        v
191    }
192
193    #[test]
194    fn encodes_begin_request() {
195        let desc = [1, 2, 3, 4, 5, 6, 7, 8];
196        let req = TransactionManagerRequest::begin(desc, IsolationLevel::ReadCommitted, "");
197
198        let mut buf = BytesMut::new();
199        req.encode(&mut buf).unwrap();
200
201        let mut expected = all_headers();
202        expected.extend_from_slice(&(TransactionManagerRequestType::Begin as u16).to_le_bytes());
203        expected.push(IsolationLevel::ReadCommitted as u8); // isolation level
204        expected.push(0); // B_VARCHAR length (empty name)
205
206        assert_eq!(&buf[..], &expected[..]);
207    }
208
209    #[test]
210    fn encodes_begin_request_with_name() {
211        let desc = [1, 2, 3, 4, 5, 6, 7, 8];
212        let req = TransactionManagerRequest::begin(desc, IsolationLevel::Serializable, "tx");
213
214        let mut buf = BytesMut::new();
215        req.encode(&mut buf).unwrap();
216
217        let mut expected = all_headers();
218        expected.extend_from_slice(&(TransactionManagerRequestType::Begin as u16).to_le_bytes());
219        expected.push(IsolationLevel::Serializable as u8);
220        expected.push(2); // two UTF-16 code units
221        expected.extend_from_slice(&b't'.to_le_bytes());
222        expected.push(0);
223        expected.extend_from_slice(&b'x'.to_le_bytes());
224        expected.push(0);
225
226        assert_eq!(&buf[..], &expected[..]);
227    }
228
229    #[test]
230    fn encodes_commit_request() {
231        let desc = [1, 2, 3, 4, 5, 6, 7, 8];
232        let req = TransactionManagerRequest::commit(desc, "");
233
234        let mut buf = BytesMut::new();
235        req.encode(&mut buf).unwrap();
236
237        let mut expected = all_headers();
238        expected.extend_from_slice(&(TransactionManagerRequestType::Commit as u16).to_le_bytes());
239        expected.push(0); // B_VARCHAR length (empty name)
240        expected.push(0); // flags: no new transaction
241
242        assert_eq!(&buf[..], &expected[..]);
243    }
244
245    #[test]
246    fn encodes_rollback_request() {
247        let desc = [8, 7, 6, 5, 4, 3, 2, 1];
248        let req = TransactionManagerRequest::rollback(desc, "");
249
250        let mut buf = BytesMut::new();
251        req.encode(&mut buf).unwrap();
252
253        let mut expected = Vec::new();
254        expected.extend_from_slice(&(ALL_HEADERS_LEN_TX as u32).to_le_bytes());
255        expected.extend_from_slice(&(ALL_HEADERS_LEN_TX as u32 - 4).to_le_bytes());
256        expected.extend_from_slice(&(AllHeaderTy::TransactionDescriptor as u16).to_le_bytes());
257        expected.extend_from_slice(&desc);
258        expected.extend_from_slice(&1u32.to_le_bytes());
259        expected.extend_from_slice(&(TransactionManagerRequestType::Rollback as u16).to_le_bytes());
260        expected.push(0); // B_VARCHAR length
261        expected.push(0); // flags
262
263        assert_eq!(&buf[..], &expected[..]);
264    }
265
266    #[test]
267    fn encodes_save_request() {
268        let desc = [1, 2, 3, 4, 5, 6, 7, 8];
269        let req = TransactionManagerRequest::save(desc, "sp1");
270
271        let mut buf = BytesMut::new();
272        req.encode(&mut buf).unwrap();
273
274        let mut expected = all_headers();
275        expected.extend_from_slice(&(TransactionManagerRequestType::Save as u16).to_le_bytes());
276        expected.push(3); // three UTF-16 code units
277        for unit in "sp1".encode_utf16() {
278            expected.extend_from_slice(&unit.to_le_bytes());
279        }
280
281        assert_eq!(&buf[..], &expected[..]);
282    }
283}