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/// A snapshot exported by a running transaction (`pg_export_snapshot()`),
50/// for another transaction to read the same data with
51/// [`ConfigBuilder::snapshot`].
52///
53/// Get one from a transaction's `snapshot()`, or parse the text form
54/// with [`SnapshotId::new`]. It stays importable only while the exporting
55/// transaction is open; after that `PostgreSQL` rejects it. The id is a
56/// small `Copy` value, so it can be handed to other threads and tasks.
57#[derive(Clone, Copy, PartialEq, Eq, Hash)]
58pub struct SnapshotId {
59    bytes: [u8; SnapshotId::CAPACITY],
60    len: u8,
61}
62
63impl SnapshotId {
64    /// The longest id accepted. `PostgreSQL` ids are up to about 30
65    /// characters (`00000003-0000001B-1`).
66    const CAPACITY: usize = 64;
67
68    /// Parses a snapshot id: hex digits and `-`, as `pg_export_snapshot()`
69    /// returns it. Returns `None` for anything else, so an id is always safe
70    /// to write into `SET TRANSACTION SNAPSHOT`.
71    #[must_use]
72    pub const fn new(id: &str) -> Option<Self> {
73        let source = id.as_bytes();
74        if source.is_empty() || source.len() > Self::CAPACITY {
75            return None;
76        }
77        let mut bytes = [0u8; Self::CAPACITY];
78        let mut i = 0;
79        while i < source.len() {
80            let byte = source[i];
81            if !(byte.is_ascii_hexdigit() || byte == b'-') {
82                return None;
83            }
84            bytes[i] = byte;
85            i += 1;
86        }
87        // CAPACITY fits a u8.
88        #[allow(clippy::cast_possible_truncation)]
89        let len = source.len() as u8;
90        Some(Self { bytes, len })
91    }
92
93    /// The id as `PostgreSQL` writes it.
94    #[must_use]
95    pub fn as_str(&self) -> &str {
96        // `new` only stores ASCII.
97        core::str::from_utf8(&self.bytes[..usize::from(self.len)]).unwrap_or_default()
98    }
99}
100
101impl core::fmt::Debug for SnapshotId {
102    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103        formatter
104            .debug_tuple("SnapshotId")
105            .field(&self.as_str())
106            .finish()
107    }
108}
109
110impl core::fmt::Display for SnapshotId {
111    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
112        formatter.write_str(self.as_str())
113    }
114}
115
116/// Options for starting a `PostgreSQL` transaction: isolation level, access
117/// mode, `DEFERRABLE`, and a snapshot to read from.
118///
119/// The default leaves every choice to the server. Use [`Self::builder`] when
120/// the choices are known in code; it only offers `.deferrable()` after
121/// `.serializable().read_only()`, the only combination where `PostgreSQL`
122/// gives `DEFERRABLE` a meaning. Use the setters on this type for choices
123/// made at runtime.
124///
125/// # Examples
126///
127/// ```
128/// use drizzle_postgres::{AccessMode, IsolationLevel, TransactionConfig};
129///
130/// let config = TransactionConfig::builder()
131///     .serializable()
132///     .read_only()
133///     .deferrable()
134///     .build();
135/// assert_eq!(config.isolation(), Some(IsolationLevel::Serializable));
136/// assert_eq!(config.access(), Some(AccessMode::ReadOnly));
137/// assert!(config.is_deferrable());
138///
139/// // Runtime choices:
140/// let config = TransactionConfig::new().isolation_level(IsolationLevel::RepeatableRead);
141/// assert_eq!(config.access(), None); // server default
142/// ```
143///
144/// # Compile-time checks
145///
146/// ```compile_fail
147/// use drizzle_postgres::TransactionConfig;
148///
149/// // DEFERRABLE only has meaning for a serializable, read-only transaction.
150/// let _ = TransactionConfig::builder()
151///     .serializable()
152///     .read_write()
153///     .deferrable();
154/// ```
155///
156/// ```compile_fail
157/// use drizzle_postgres::{SnapshotId, TransactionConfig};
158///
159/// // PostgreSQL imports a snapshot only into REPEATABLE READ or SERIALIZABLE.
160/// let snapshot = SnapshotId::new("00000003-0000001B-1").unwrap();
161/// let _ = TransactionConfig::builder()
162///     .read_committed()
163///     .snapshot(snapshot);
164/// ```
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
166pub struct TransactionConfig {
167    isolation_level: Option<IsolationLevel>,
168    access_mode: Option<AccessMode>,
169    deferrable: bool,
170    snapshot: Option<SnapshotId>,
171    legacy_read_committed: bool,
172}
173
174impl TransactionConfig {
175    /// Uses server defaults for every option.
176    #[must_use]
177    pub const fn new() -> Self {
178        Self {
179            isolation_level: None,
180            access_mode: None,
181            deferrable: false,
182            snapshot: None,
183            legacy_read_committed: false,
184        }
185    }
186
187    /// Starts a [`ConfigBuilder`], which checks option combinations at compile time.
188    pub const fn builder() -> ConfigBuilder {
189        ConfigBuilder::new()
190    }
191
192    /// Sets the isolation level.
193    ///
194    /// Any level other than `SERIALIZABLE` turns `DEFERRABLE` off, and one
195    /// below `REPEATABLE READ` drops the snapshot.
196    #[must_use]
197    pub const fn isolation_level(mut self, level: IsolationLevel) -> Self {
198        self.isolation_level = Some(level);
199        if !matches!(level, IsolationLevel::Serializable) {
200            self.deferrable = false;
201        }
202        if !matches!(
203            level,
204            IsolationLevel::RepeatableRead | IsolationLevel::Serializable
205        ) {
206            self.snapshot = None;
207        }
208        self.legacy_read_committed = false;
209        self
210    }
211
212    /// Sets the access mode.
213    ///
214    /// `READ WRITE` turns `DEFERRABLE` off.
215    #[must_use]
216    pub const fn access_mode(mut self, mode: AccessMode) -> Self {
217        self.access_mode = Some(mode);
218        if !matches!(mode, AccessMode::ReadOnly) {
219            self.deferrable = false;
220        }
221        self
222    }
223
224    /// Makes the transaction `SERIALIZABLE READ ONLY DEFERRABLE`.
225    ///
226    /// `DEFERRABLE` only has a meaning for a serializable, read-only
227    /// transaction, so this also sets those two options. Prefer the builder
228    /// when the choices are known in code.
229    #[must_use]
230    pub const fn deferrable(mut self) -> Self {
231        self.isolation_level = Some(IsolationLevel::Serializable);
232        self.access_mode = Some(AccessMode::ReadOnly);
233        self.deferrable = true;
234        self.legacy_read_committed = false;
235        self
236    }
237
238    /// Returns the isolation level, or `None` for the server default.
239    #[must_use]
240    pub const fn isolation(&self) -> Option<IsolationLevel> {
241        self.isolation_level
242    }
243
244    /// Returns the access mode, or `None` for the server default.
245    #[must_use]
246    pub const fn access(&self) -> Option<AccessMode> {
247        self.access_mode
248    }
249
250    /// Returns `true` if `DEFERRABLE` is set.
251    #[must_use]
252    pub const fn is_deferrable(&self) -> bool {
253        self.deferrable
254    }
255
256    /// The snapshot the transaction reads from, if any.
257    #[must_use]
258    pub const fn snapshot(&self) -> Option<SnapshotId> {
259        self.snapshot
260    }
261
262    /// Whether wire-protocol adapters should preserve the legacy
263    /// server-default behavior of `PostgresTransactionType::ReadCommitted`.
264    #[doc(hidden)]
265    #[must_use]
266    pub const fn uses_server_default_isolation(&self) -> bool {
267        self.isolation_level.is_none() || self.legacy_read_committed
268    }
269}
270
271impl From<PostgresTransactionType> for TransactionConfig {
272    fn from(tx_type: PostgresTransactionType) -> Self {
273        let isolation_level = match tx_type {
274            PostgresTransactionType::ReadCommitted => Some(IsolationLevel::ReadCommitted),
275            PostgresTransactionType::ReadUncommitted => Some(IsolationLevel::ReadUncommitted),
276            PostgresTransactionType::RepeatableRead => Some(IsolationLevel::RepeatableRead),
277            PostgresTransactionType::Serializable => Some(IsolationLevel::Serializable),
278        };
279        Self {
280            isolation_level,
281            legacy_read_committed: matches!(tx_type, PostgresTransactionType::ReadCommitted),
282            ..Self::new()
283        }
284    }
285}
286
287/// Builds a [`TransactionConfig`], rejecting meaningless combinations at compile time.
288///
289/// Start with [`TransactionConfig::builder`], pick an isolation level and an
290/// access mode in either order, then call [`build`](Self::build). The type
291/// parameters track the choices and are inferred.
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293#[must_use]
294pub struct ConfigBuilder<Isolation = state::ServerDefault, Access = state::ServerDefault> {
295    config: TransactionConfig,
296    state: PhantomData<(Isolation, Access)>,
297}
298
299impl ConfigBuilder {
300    const fn new() -> Self {
301        Self {
302            config: TransactionConfig::new(),
303            state: PhantomData,
304        }
305    }
306}
307
308impl<Isolation, Access> ConfigBuilder<Isolation, Access> {
309    const fn isolation<Next>(mut self, level: IsolationLevel) -> ConfigBuilder<Next, Access> {
310        self.config = self.config.isolation_level(level);
311        ConfigBuilder {
312            config: self.config,
313            state: PhantomData,
314        }
315    }
316
317    const fn access<Next>(mut self, mode: AccessMode) -> ConfigBuilder<Isolation, Next> {
318        self.config.access_mode = Some(mode);
319        if !matches!(mode, AccessMode::ReadOnly) {
320            self.config.deferrable = false;
321        }
322        ConfigBuilder {
323            config: self.config,
324            state: PhantomData,
325        }
326    }
327
328    /// Selects an isolation level supplied at runtime.
329    pub const fn isolation_level(
330        self,
331        level: IsolationLevel,
332    ) -> ConfigBuilder<state::Dynamic, Access> {
333        self.isolation(level)
334    }
335
336    /// Uses `READ UNCOMMITTED` isolation.
337    pub const fn read_uncommitted(self) -> ConfigBuilder<state::ReadUncommitted, Access> {
338        self.isolation(IsolationLevel::ReadUncommitted)
339    }
340
341    /// Uses `READ COMMITTED` isolation.
342    pub const fn read_committed(self) -> ConfigBuilder<state::ReadCommitted, Access> {
343        self.isolation(IsolationLevel::ReadCommitted)
344    }
345
346    /// Uses `REPEATABLE READ` isolation.
347    pub const fn repeatable_read(self) -> ConfigBuilder<state::RepeatableRead, Access> {
348        self.isolation(IsolationLevel::RepeatableRead)
349    }
350
351    /// Uses `SERIALIZABLE` isolation.
352    pub const fn serializable(self) -> ConfigBuilder<state::Serializable, Access> {
353        self.isolation(IsolationLevel::Serializable)
354    }
355
356    /// Selects an access mode supplied at runtime.
357    pub const fn access_mode(self, mode: AccessMode) -> ConfigBuilder<Isolation, state::Dynamic> {
358        self.access(mode)
359    }
360
361    /// Rejects writes in the transaction.
362    pub const fn read_only(self) -> ConfigBuilder<Isolation, state::ReadOnly> {
363        self.access(AccessMode::ReadOnly)
364    }
365
366    /// Permits reads and writes in the transaction.
367    pub const fn read_write(self) -> ConfigBuilder<Isolation, state::ReadWrite> {
368        self.access(AccessMode::ReadWrite)
369    }
370
371    /// Returns the finished [`TransactionConfig`].
372    #[must_use]
373    pub const fn build(self) -> TransactionConfig {
374        self.config
375    }
376}
377
378impl<Access> ConfigBuilder<state::RepeatableRead, Access> {
379    /// Reads from `snapshot`, exported by another open transaction, so both
380    /// see the same data (`SET TRANSACTION SNAPSHOT`).
381    ///
382    /// Only available with `REPEATABLE READ` or `SERIALIZABLE`, the levels
383    /// `PostgreSQL` imports a snapshot into.
384    pub const fn snapshot(mut self, snapshot: SnapshotId) -> Self {
385        self.config.snapshot = Some(snapshot);
386        self
387    }
388}
389
390impl<Access> ConfigBuilder<state::Serializable, Access> {
391    /// Reads from `snapshot`, exported by another open transaction, so both
392    /// see the same data (`SET TRANSACTION SNAPSHOT`).
393    ///
394    /// Only available with `REPEATABLE READ` or `SERIALIZABLE`, the levels
395    /// `PostgreSQL` imports a snapshot into.
396    pub const fn snapshot(mut self, snapshot: SnapshotId) -> Self {
397        self.config.snapshot = Some(snapshot);
398        self
399    }
400}
401
402impl ConfigBuilder<state::Serializable, state::ReadOnly> {
403    /// Adds `DEFERRABLE`: the transaction waits for a snapshot that cannot
404    /// cause a serialization failure, then runs without that risk.
405    ///
406    /// Only available after `.serializable()` and `.read_only()`.
407    pub const fn deferrable(mut self) -> Self {
408        self.config.deferrable = true;
409        self
410    }
411}
412
413/// Typestate markers used by [`ConfigBuilder`].
414#[doc(hidden)]
415pub mod state {
416    /// Server-selected option.
417    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
418    pub struct ServerDefault;
419    /// Option supplied at runtime.
420    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
421    pub struct Dynamic;
422    /// `READ UNCOMMITTED` isolation.
423    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
424    pub struct ReadUncommitted;
425    /// `READ COMMITTED` isolation.
426    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
427    pub struct ReadCommitted;
428    /// `REPEATABLE READ` isolation.
429    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
430    pub struct RepeatableRead;
431    /// `SERIALIZABLE` isolation.
432    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
433    pub struct Serializable;
434    /// Read-only access.
435    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
436    pub struct ReadOnly;
437    /// Read-write access.
438    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
439    pub struct ReadWrite;
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445
446    #[test]
447    fn default_uses_server_policy() {
448        assert_eq!(TransactionConfig::new(), TransactionConfig::default());
449    }
450
451    #[test]
452    fn builder_preserves_selected_options() {
453        let config = TransactionConfig::builder()
454            .serializable()
455            .read_only()
456            .deferrable()
457            .build();
458
459        assert_eq!(config.isolation(), Some(IsolationLevel::Serializable));
460        assert_eq!(config.access(), Some(AccessMode::ReadOnly));
461        assert!(config.is_deferrable());
462    }
463
464    #[test]
465    fn legacy_read_committed_preserves_adapter_behavior() {
466        let config = TransactionConfig::from(PostgresTransactionType::ReadCommitted);
467        assert_eq!(config.isolation(), Some(IsolationLevel::ReadCommitted));
468        assert!(config.uses_server_default_isolation());
469    }
470
471    #[test]
472    fn changing_access_clears_deferrable() {
473        let config = TransactionConfig::builder()
474            .serializable()
475            .read_only()
476            .deferrable()
477            .read_write()
478            .build();
479
480        assert_eq!(config.access(), Some(AccessMode::ReadWrite));
481        assert!(!config.is_deferrable());
482    }
483
484    #[test]
485    fn runtime_setters_clear_invalid_deferrable_state() {
486        let config = TransactionConfig::builder()
487            .serializable()
488            .read_only()
489            .deferrable()
490            .build()
491            .access_mode(AccessMode::ReadWrite)
492            .isolation_level(IsolationLevel::ReadCommitted);
493
494        assert!(!config.is_deferrable());
495        assert!(!config.uses_server_default_isolation());
496    }
497
498    #[test]
499    fn compatible_runtime_setters_preserve_deferrable() {
500        let config = TransactionConfig::new()
501            .deferrable()
502            .isolation_level(IsolationLevel::Serializable)
503            .access_mode(AccessMode::ReadOnly);
504
505        assert!(config.is_deferrable());
506    }
507
508    #[test]
509    fn compatible_builder_transitions_preserve_deferrable() {
510        let config = TransactionConfig::builder()
511            .serializable()
512            .read_only()
513            .deferrable()
514            .serializable()
515            .read_only()
516            .build();
517
518        assert!(config.is_deferrable());
519    }
520
521    #[test]
522    fn access_mode_preserves_legacy_server_default_isolation() {
523        let config = TransactionConfig::from(PostgresTransactionType::ReadCommitted)
524            .access_mode(AccessMode::ReadOnly);
525
526        assert_eq!(config.isolation(), Some(IsolationLevel::ReadCommitted));
527        assert!(config.uses_server_default_isolation());
528    }
529
530    #[test]
531    fn runtime_deferrable_selects_valid_modes() {
532        let config = TransactionConfig::new().deferrable();
533
534        assert_eq!(config.isolation(), Some(IsolationLevel::Serializable));
535        assert_eq!(config.access(), Some(AccessMode::ReadOnly));
536        assert!(config.is_deferrable());
537        assert!(!config.uses_server_default_isolation());
538    }
539
540    #[test]
541    fn snapshot_ids_hold_only_hex_digits_and_dashes() {
542        let id = SnapshotId::new("00000003-0000001B-1").unwrap();
543        assert_eq!(id.as_str(), "00000003-0000001B-1");
544        assert_eq!(id.to_string(), "00000003-0000001B-1");
545
546        assert_eq!(SnapshotId::new(""), None);
547        assert_eq!(SnapshotId::new("0000-1'; DROP TABLE users; --"), None);
548        assert_eq!(SnapshotId::new(&"0".repeat(65)), None);
549    }
550
551    #[test]
552    fn lowering_the_isolation_level_drops_the_snapshot() {
553        let snapshot = SnapshotId::new("00000003-0000001B-1").unwrap();
554        let config = TransactionConfig::builder()
555            .repeatable_read()
556            .snapshot(snapshot)
557            .serializable()
558            .build();
559        assert_eq!(config.snapshot(), Some(snapshot));
560
561        let config = config.isolation_level(IsolationLevel::ReadCommitted);
562        assert_eq!(config.snapshot(), None);
563    }
564}