Skip to main content

drizzle_mysql/
transaction.rs

1//! `MySQL` transaction options: isolation level, access mode and consistent
2//! snapshot. The `drizzle` crate's `MySQL` drivers take a
3//! [`TransactionConfig`] when starting a transaction.
4
5use core::marker::PhantomData;
6
7/// MySQL transaction isolation level.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum IsolationLevel {
10    /// `READ UNCOMMITTED`.
11    ReadUncommitted,
12    /// `READ COMMITTED`.
13    ReadCommitted,
14    /// `REPEATABLE READ` (the InnoDB default).
15    RepeatableRead,
16    /// `SERIALIZABLE`.
17    Serializable,
18}
19
20impl core::fmt::Display for IsolationLevel {
21    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
22        formatter.write_str(match self {
23            Self::ReadUncommitted => "READ UNCOMMITTED",
24            Self::ReadCommitted => "READ COMMITTED",
25            Self::RepeatableRead => "REPEATABLE READ",
26            Self::Serializable => "SERIALIZABLE",
27        })
28    }
29}
30
31/// MySQL transaction access mode.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum AccessMode {
34    /// Reject writes in the transaction.
35    ReadOnly,
36    /// Permit reads and writes.
37    ReadWrite,
38}
39
40impl core::fmt::Display for AccessMode {
41    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
42        formatter.write_str(match self {
43            Self::ReadOnly => "READ ONLY",
44            Self::ReadWrite => "READ WRITE",
45        })
46    }
47}
48
49/// Options applied when starting a MySQL transaction.
50///
51/// Use [`TransactionConfig::builder`] when the choices are known statically.
52/// Its typestate only exposes [`ConfigBuilder::snapshot`] after
53/// [`ConfigBuilder::repeatable_read`], matching MySQL's requirement for a
54/// consistent snapshot. The direct setters remain useful when values come
55/// from runtime configuration.
56///
57/// # Examples
58///
59/// ```
60/// use drizzle_mysql::{AccessMode, IsolationLevel, TransactionConfig};
61///
62/// let config = TransactionConfig::builder()
63///     .repeatable_read()
64///     .read_only()
65///     .snapshot()
66///     .build();
67///
68/// assert_eq!(config.isolation(), Some(IsolationLevel::RepeatableRead));
69/// assert_eq!(config.access(), Some(AccessMode::ReadOnly));
70/// assert!(config.consistent_snapshot());
71/// ```
72///
73/// ```compile_fail
74/// use drizzle_mysql::TransactionConfig;
75///
76/// // MySQL cannot provide this snapshot under SERIALIZABLE isolation.
77/// let _ = TransactionConfig::builder()
78///     .serializable()
79///     .snapshot();
80/// ```
81///
82/// Concrete adapters translate this value into their client's transaction
83/// options and remain responsible for server/engine capability errors. Nested
84/// transactions are implemented with savepoints by the adapter, not by
85/// starting another wire-driver transaction.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
87pub struct TransactionConfig {
88    isolation_level: Option<IsolationLevel>,
89    access_mode: Option<AccessMode>,
90    consistent_snapshot: bool,
91}
92
93impl TransactionConfig {
94    /// Uses server defaults for every option.
95    #[must_use]
96    pub const fn new() -> Self {
97        Self {
98            isolation_level: None,
99            access_mode: None,
100            consistent_snapshot: false,
101        }
102    }
103
104    /// Starts a typestated transaction configuration.
105    pub const fn builder() -> ConfigBuilder {
106        ConfigBuilder::new()
107    }
108
109    /// Selects an isolation level supplied at runtime.
110    ///
111    /// Any level other than `REPEATABLE READ` clears a requested consistent
112    /// snapshot.
113    #[must_use]
114    pub const fn isolation_level(mut self, level: IsolationLevel) -> Self {
115        self.isolation_level = Some(level);
116        if !matches!(level, IsolationLevel::RepeatableRead) {
117            self.consistent_snapshot = false;
118        }
119        self
120    }
121
122    /// Selects read-only or read-write access supplied at runtime.
123    #[must_use]
124    pub const fn access_mode(mut self, mode: AccessMode) -> Self {
125        self.access_mode = Some(mode);
126        self
127    }
128
129    /// Requests `WITH CONSISTENT SNAPSHOT` for runtime-derived configuration.
130    ///
131    /// This selects `REPEATABLE READ`, the isolation level where MySQL gives
132    /// the option its documented meaning. Prefer the typestated builder when
133    /// the isolation level is known in code.
134    #[must_use]
135    pub const fn with_consistent_snapshot(mut self) -> Self {
136        self.isolation_level = Some(IsolationLevel::RepeatableRead);
137        self.consistent_snapshot = true;
138        self
139    }
140
141    /// Configured isolation level, or `None` to use the server default.
142    #[must_use]
143    pub const fn isolation(&self) -> Option<IsolationLevel> {
144        self.isolation_level
145    }
146
147    /// Configured access mode, or `None` to use the server default.
148    #[must_use]
149    pub const fn access(&self) -> Option<AccessMode> {
150        self.access_mode
151    }
152
153    /// Whether a consistent snapshot was requested.
154    #[must_use]
155    pub const fn consistent_snapshot(&self) -> bool {
156        self.consistent_snapshot
157    }
158}
159
160/// Typestated builder for [`TransactionConfig`].
161///
162/// The state parameter is inferred and does not need to be named by callers.
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164#[must_use]
165pub struct ConfigBuilder<Isolation = state::ServerDefault> {
166    config: TransactionConfig,
167    state: PhantomData<Isolation>,
168}
169
170impl ConfigBuilder {
171    const fn new() -> Self {
172        Self {
173            config: TransactionConfig::new(),
174            state: PhantomData,
175        }
176    }
177}
178
179impl<Isolation> ConfigBuilder<Isolation> {
180    const fn isolation<Next>(mut self, level: IsolationLevel) -> ConfigBuilder<Next> {
181        self.config.isolation_level = Some(level);
182        if !matches!(level, IsolationLevel::RepeatableRead) {
183            self.config.consistent_snapshot = false;
184        }
185        ConfigBuilder {
186            config: self.config,
187            state: PhantomData,
188        }
189    }
190
191    /// Selects an isolation level supplied at runtime.
192    ///
193    /// Because the level is not known at compile time,
194    /// [`snapshot`](ConfigBuilder::snapshot) is not available afterwards; use
195    /// [`TransactionConfig::with_consistent_snapshot`] instead.
196    pub const fn isolation_level(self, level: IsolationLevel) -> ConfigBuilder<state::Dynamic> {
197        self.isolation(level)
198    }
199
200    /// Uses `READ UNCOMMITTED` isolation.
201    pub const fn read_uncommitted(self) -> ConfigBuilder<state::ReadUncommitted> {
202        self.isolation(IsolationLevel::ReadUncommitted)
203    }
204
205    /// Uses `READ COMMITTED` isolation.
206    pub const fn read_committed(self) -> ConfigBuilder<state::ReadCommitted> {
207        self.isolation(IsolationLevel::ReadCommitted)
208    }
209
210    /// Uses `REPEATABLE READ` isolation.
211    pub const fn repeatable_read(self) -> ConfigBuilder<state::RepeatableRead> {
212        self.isolation(IsolationLevel::RepeatableRead)
213    }
214
215    /// Uses `SERIALIZABLE` isolation.
216    pub const fn serializable(self) -> ConfigBuilder<state::Serializable> {
217        self.isolation(IsolationLevel::Serializable)
218    }
219
220    /// Rejects writes in the transaction.
221    pub const fn read_only(mut self) -> Self {
222        self.config.access_mode = Some(AccessMode::ReadOnly);
223        self
224    }
225
226    /// Permits reads and writes in the transaction.
227    pub const fn read_write(mut self) -> Self {
228        self.config.access_mode = Some(AccessMode::ReadWrite);
229        self
230    }
231
232    /// Finishes the configuration.
233    #[must_use]
234    pub const fn build(self) -> TransactionConfig {
235        self.config
236    }
237}
238
239impl ConfigBuilder<state::RepeatableRead> {
240    /// Requests `START TRANSACTION WITH CONSISTENT SNAPSHOT`.
241    ///
242    /// Only available after [`repeatable_read`](ConfigBuilder::repeatable_read).
243    /// Selecting another isolation level afterwards clears it.
244    pub const fn snapshot(mut self) -> Self {
245        self.config.consistent_snapshot = true;
246        self
247    }
248}
249
250/// Typestate markers used by [`ConfigBuilder`].
251#[doc(hidden)]
252pub mod state {
253    /// Server-selected isolation.
254    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
255    pub struct ServerDefault;
256    /// Isolation supplied at runtime.
257    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
258    pub struct Dynamic;
259    /// `READ UNCOMMITTED` isolation.
260    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
261    pub struct ReadUncommitted;
262    /// `READ COMMITTED` isolation.
263    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
264    pub struct ReadCommitted;
265    /// `REPEATABLE READ` isolation.
266    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
267    pub struct RepeatableRead;
268    /// `SERIALIZABLE` isolation.
269    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
270    pub struct Serializable;
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn transaction_options_default_to_server_policy() {
279        assert_eq!(TransactionConfig::new(), TransactionConfig::default());
280    }
281
282    #[test]
283    fn builder_preserves_selected_options() {
284        let config = TransactionConfig::builder()
285            .repeatable_read()
286            .read_only()
287            .snapshot()
288            .build();
289
290        assert_eq!(config.isolation(), Some(IsolationLevel::RepeatableRead));
291        assert_eq!(config.access(), Some(AccessMode::ReadOnly));
292        assert!(config.consistent_snapshot());
293    }
294
295    #[test]
296    fn runtime_options_remain_available() {
297        let config = TransactionConfig::new()
298            .isolation_level(IsolationLevel::Serializable)
299            .access_mode(AccessMode::ReadWrite)
300            .with_consistent_snapshot();
301
302        assert_eq!(config.isolation(), Some(IsolationLevel::RepeatableRead));
303        assert_eq!(config.access(), Some(AccessMode::ReadWrite));
304        assert!(config.consistent_snapshot());
305    }
306
307    #[test]
308    fn runtime_isolation_change_clears_snapshot() {
309        let config = TransactionConfig::new()
310            .with_consistent_snapshot()
311            .isolation_level(IsolationLevel::Serializable);
312
313        assert_eq!(config.isolation(), Some(IsolationLevel::Serializable));
314        assert!(!config.consistent_snapshot());
315    }
316
317    #[test]
318    fn repeated_runtime_isolation_preserves_snapshot() {
319        let config = TransactionConfig::new()
320            .with_consistent_snapshot()
321            .isolation_level(IsolationLevel::RepeatableRead);
322
323        assert!(config.consistent_snapshot());
324    }
325
326    #[test]
327    fn changing_isolation_clears_snapshot() {
328        let config = TransactionConfig::builder()
329            .repeatable_read()
330            .snapshot()
331            .serializable()
332            .build();
333
334        assert_eq!(config.isolation(), Some(IsolationLevel::Serializable));
335        assert!(!config.consistent_snapshot());
336    }
337
338    #[test]
339    fn repeated_builder_isolation_preserves_snapshot() {
340        let config = TransactionConfig::builder()
341            .repeatable_read()
342            .snapshot()
343            .repeatable_read()
344            .build();
345
346        assert!(config.consistent_snapshot());
347    }
348}