Skip to main content

drizzle_postgres/
transaction.rs

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