Skip to main content

drizzle_postgres/
transaction.rs

1//! Driver-neutral `PostgreSQL` transaction options ([`TransactionConfig`]).
2
3use core::marker::PhantomData;
4
5use crate::common::PostgresTransactionType;
6
7/// PostgreSQL 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`.
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/// PostgreSQL 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 for starting a `PostgreSQL` transaction: isolation level, access
50/// mode and `DEFERRABLE`.
51///
52/// The default leaves every choice to the server. Use [`Self::builder`] when
53/// the choices are known in code; it only offers `.deferrable()` after
54/// `.serializable().read_only()`, the only combination where `PostgreSQL`
55/// gives `DEFERRABLE` a meaning. Use the setters on this type for choices
56/// made at runtime.
57///
58/// # Examples
59///
60/// ```
61/// use drizzle_postgres::{AccessMode, IsolationLevel, TransactionConfig};
62///
63/// let config = TransactionConfig::builder()
64///     .serializable()
65///     .read_only()
66///     .deferrable()
67///     .build();
68/// assert_eq!(config.isolation(), Some(IsolationLevel::Serializable));
69/// assert_eq!(config.access(), Some(AccessMode::ReadOnly));
70/// assert!(config.is_deferrable());
71///
72/// // Runtime choices:
73/// let config = TransactionConfig::new().isolation_level(IsolationLevel::RepeatableRead);
74/// assert_eq!(config.access(), None); // server default
75/// ```
76///
77/// # Compile-time checks
78///
79/// ```compile_fail
80/// use drizzle_postgres::TransactionConfig;
81///
82/// // DEFERRABLE only has meaning for a serializable, read-only transaction.
83/// let _ = TransactionConfig::builder()
84///     .serializable()
85///     .read_write()
86///     .deferrable();
87/// ```
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
89pub struct TransactionConfig {
90    isolation_level: Option<IsolationLevel>,
91    access_mode: Option<AccessMode>,
92    deferrable: bool,
93    legacy_read_committed: bool,
94}
95
96impl TransactionConfig {
97    /// Uses server defaults for every option.
98    #[must_use]
99    pub const fn new() -> Self {
100        Self {
101            isolation_level: None,
102            access_mode: None,
103            deferrable: false,
104            legacy_read_committed: false,
105        }
106    }
107
108    /// Starts a [`ConfigBuilder`], which checks option combinations at compile time.
109    pub const fn builder() -> ConfigBuilder {
110        ConfigBuilder::new()
111    }
112
113    /// Sets the isolation level.
114    ///
115    /// Any level other than `SERIALIZABLE` turns `DEFERRABLE` off.
116    #[must_use]
117    pub const fn isolation_level(mut self, level: IsolationLevel) -> Self {
118        self.isolation_level = Some(level);
119        if !matches!(level, IsolationLevel::Serializable) {
120            self.deferrable = false;
121        }
122        self.legacy_read_committed = false;
123        self
124    }
125
126    /// Sets the access mode.
127    ///
128    /// `READ WRITE` turns `DEFERRABLE` off.
129    #[must_use]
130    pub const fn access_mode(mut self, mode: AccessMode) -> Self {
131        self.access_mode = Some(mode);
132        if !matches!(mode, AccessMode::ReadOnly) {
133            self.deferrable = false;
134        }
135        self
136    }
137
138    /// Makes the transaction `SERIALIZABLE READ ONLY DEFERRABLE`.
139    ///
140    /// `DEFERRABLE` only has a meaning for a serializable, read-only
141    /// transaction, so this also sets those two options. Prefer the builder
142    /// when the choices are known in code.
143    #[must_use]
144    pub const fn deferrable(mut self) -> Self {
145        self.isolation_level = Some(IsolationLevel::Serializable);
146        self.access_mode = Some(AccessMode::ReadOnly);
147        self.deferrable = true;
148        self.legacy_read_committed = false;
149        self
150    }
151
152    /// Returns the isolation level, or `None` for the server default.
153    #[must_use]
154    pub const fn isolation(&self) -> Option<IsolationLevel> {
155        self.isolation_level
156    }
157
158    /// Returns the access mode, or `None` for the server default.
159    #[must_use]
160    pub const fn access(&self) -> Option<AccessMode> {
161        self.access_mode
162    }
163
164    /// Returns `true` if `DEFERRABLE` is set.
165    #[must_use]
166    pub const fn is_deferrable(&self) -> bool {
167        self.deferrable
168    }
169
170    /// Whether wire-protocol adapters should preserve the legacy
171    /// server-default behavior of `PostgresTransactionType::ReadCommitted`.
172    #[doc(hidden)]
173    #[must_use]
174    pub const fn uses_server_default_isolation(&self) -> bool {
175        self.isolation_level.is_none() || self.legacy_read_committed
176    }
177}
178
179impl From<PostgresTransactionType> for TransactionConfig {
180    fn from(tx_type: PostgresTransactionType) -> Self {
181        let isolation_level = match tx_type {
182            PostgresTransactionType::ReadCommitted => Some(IsolationLevel::ReadCommitted),
183            PostgresTransactionType::ReadUncommitted => Some(IsolationLevel::ReadUncommitted),
184            PostgresTransactionType::RepeatableRead => Some(IsolationLevel::RepeatableRead),
185            PostgresTransactionType::Serializable => Some(IsolationLevel::Serializable),
186        };
187        Self {
188            isolation_level,
189            legacy_read_committed: matches!(tx_type, PostgresTransactionType::ReadCommitted),
190            ..Self::new()
191        }
192    }
193}
194
195/// Builds a [`TransactionConfig`], rejecting meaningless combinations at compile time.
196///
197/// Start with [`TransactionConfig::builder`], pick an isolation level and an
198/// access mode in either order, then call [`build`](Self::build). The type
199/// parameters track the choices and are inferred.
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201#[must_use]
202pub struct ConfigBuilder<Isolation = state::ServerDefault, Access = state::ServerDefault> {
203    config: TransactionConfig,
204    state: PhantomData<(Isolation, Access)>,
205}
206
207impl ConfigBuilder {
208    const fn new() -> Self {
209        Self {
210            config: TransactionConfig::new(),
211            state: PhantomData,
212        }
213    }
214}
215
216impl<Isolation, Access> ConfigBuilder<Isolation, Access> {
217    const fn isolation<Next>(mut self, level: IsolationLevel) -> ConfigBuilder<Next, Access> {
218        self.config.isolation_level = Some(level);
219        if !matches!(level, IsolationLevel::Serializable) {
220            self.config.deferrable = false;
221        }
222        self.config.legacy_read_committed = false;
223        ConfigBuilder {
224            config: self.config,
225            state: PhantomData,
226        }
227    }
228
229    const fn access<Next>(mut self, mode: AccessMode) -> ConfigBuilder<Isolation, Next> {
230        self.config.access_mode = Some(mode);
231        if !matches!(mode, AccessMode::ReadOnly) {
232            self.config.deferrable = false;
233        }
234        ConfigBuilder {
235            config: self.config,
236            state: PhantomData,
237        }
238    }
239
240    /// Selects an isolation level supplied at runtime.
241    pub const fn isolation_level(
242        self,
243        level: IsolationLevel,
244    ) -> ConfigBuilder<state::Dynamic, Access> {
245        self.isolation(level)
246    }
247
248    /// Uses `READ UNCOMMITTED` isolation.
249    pub const fn read_uncommitted(self) -> ConfigBuilder<state::ReadUncommitted, Access> {
250        self.isolation(IsolationLevel::ReadUncommitted)
251    }
252
253    /// Uses `READ COMMITTED` isolation.
254    pub const fn read_committed(self) -> ConfigBuilder<state::ReadCommitted, Access> {
255        self.isolation(IsolationLevel::ReadCommitted)
256    }
257
258    /// Uses `REPEATABLE READ` isolation.
259    pub const fn repeatable_read(self) -> ConfigBuilder<state::RepeatableRead, Access> {
260        self.isolation(IsolationLevel::RepeatableRead)
261    }
262
263    /// Uses `SERIALIZABLE` isolation.
264    pub const fn serializable(self) -> ConfigBuilder<state::Serializable, Access> {
265        self.isolation(IsolationLevel::Serializable)
266    }
267
268    /// Selects an access mode supplied at runtime.
269    pub const fn access_mode(self, mode: AccessMode) -> ConfigBuilder<Isolation, state::Dynamic> {
270        self.access(mode)
271    }
272
273    /// Rejects writes in the transaction.
274    pub const fn read_only(self) -> ConfigBuilder<Isolation, state::ReadOnly> {
275        self.access(AccessMode::ReadOnly)
276    }
277
278    /// Permits reads and writes in the transaction.
279    pub const fn read_write(self) -> ConfigBuilder<Isolation, state::ReadWrite> {
280        self.access(AccessMode::ReadWrite)
281    }
282
283    /// Returns the finished [`TransactionConfig`].
284    #[must_use]
285    pub const fn build(self) -> TransactionConfig {
286        self.config
287    }
288}
289
290impl ConfigBuilder<state::Serializable, state::ReadOnly> {
291    /// Adds `DEFERRABLE`: the transaction waits for a snapshot that cannot
292    /// cause a serialization failure, then runs without that risk.
293    ///
294    /// Only available after `.serializable()` and `.read_only()`.
295    pub const fn deferrable(mut self) -> Self {
296        self.config.deferrable = true;
297        self
298    }
299}
300
301/// Typestate markers used by [`ConfigBuilder`].
302#[doc(hidden)]
303pub mod state {
304    /// Server-selected option.
305    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
306    pub struct ServerDefault;
307    /// Option supplied at runtime.
308    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
309    pub struct Dynamic;
310    /// `READ UNCOMMITTED` isolation.
311    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
312    pub struct ReadUncommitted;
313    /// `READ COMMITTED` isolation.
314    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
315    pub struct ReadCommitted;
316    /// `REPEATABLE READ` isolation.
317    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
318    pub struct RepeatableRead;
319    /// `SERIALIZABLE` isolation.
320    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
321    pub struct Serializable;
322    /// Read-only access.
323    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
324    pub struct ReadOnly;
325    /// Read-write access.
326    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
327    pub struct ReadWrite;
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn default_uses_server_policy() {
336        assert_eq!(TransactionConfig::new(), TransactionConfig::default());
337    }
338
339    #[test]
340    fn builder_preserves_selected_options() {
341        let config = TransactionConfig::builder()
342            .serializable()
343            .read_only()
344            .deferrable()
345            .build();
346
347        assert_eq!(config.isolation(), Some(IsolationLevel::Serializable));
348        assert_eq!(config.access(), Some(AccessMode::ReadOnly));
349        assert!(config.is_deferrable());
350    }
351
352    #[test]
353    fn legacy_read_committed_preserves_adapter_behavior() {
354        let config = TransactionConfig::from(PostgresTransactionType::ReadCommitted);
355        assert_eq!(config.isolation(), Some(IsolationLevel::ReadCommitted));
356        assert!(config.uses_server_default_isolation());
357    }
358
359    #[test]
360    fn changing_access_clears_deferrable() {
361        let config = TransactionConfig::builder()
362            .serializable()
363            .read_only()
364            .deferrable()
365            .read_write()
366            .build();
367
368        assert_eq!(config.access(), Some(AccessMode::ReadWrite));
369        assert!(!config.is_deferrable());
370    }
371
372    #[test]
373    fn runtime_setters_clear_invalid_deferrable_state() {
374        let config = TransactionConfig::builder()
375            .serializable()
376            .read_only()
377            .deferrable()
378            .build()
379            .access_mode(AccessMode::ReadWrite)
380            .isolation_level(IsolationLevel::ReadCommitted);
381
382        assert!(!config.is_deferrable());
383        assert!(!config.uses_server_default_isolation());
384    }
385
386    #[test]
387    fn compatible_runtime_setters_preserve_deferrable() {
388        let config = TransactionConfig::new()
389            .deferrable()
390            .isolation_level(IsolationLevel::Serializable)
391            .access_mode(AccessMode::ReadOnly);
392
393        assert!(config.is_deferrable());
394    }
395
396    #[test]
397    fn compatible_builder_transitions_preserve_deferrable() {
398        let config = TransactionConfig::builder()
399            .serializable()
400            .read_only()
401            .deferrable()
402            .serializable()
403            .read_only()
404            .build();
405
406        assert!(config.is_deferrable());
407    }
408
409    #[test]
410    fn access_mode_preserves_legacy_server_default_isolation() {
411        let config = TransactionConfig::from(PostgresTransactionType::ReadCommitted)
412            .access_mode(AccessMode::ReadOnly);
413
414        assert_eq!(config.isolation(), Some(IsolationLevel::ReadCommitted));
415        assert!(config.uses_server_default_isolation());
416    }
417
418    #[test]
419    fn runtime_deferrable_selects_valid_modes() {
420        let config = TransactionConfig::new().deferrable();
421
422        assert_eq!(config.isolation(), Some(IsolationLevel::Serializable));
423        assert_eq!(config.access(), Some(AccessMode::ReadOnly));
424        assert!(config.is_deferrable());
425        assert!(!config.uses_server_default_isolation());
426    }
427}