Skip to main content

modkit_db/secure/
tx_config.rs

1//! Transaction configuration types for `SecureConn`.
2//!
3//! These types abstract `SeaORM`'s transaction configuration, allowing domain
4//! and application services to specify transaction settings without importing
5//! `SeaORM` types directly.
6//!
7//! # Design Philosophy
8//!
9//! - **Isolation**: These types are exposed from `modkit_db` but do NOT expose
10//!   any `SeaORM` types. The conversion to `SeaORM` types happens internally.
11//! - **REST handlers** should never use these types directly. Transaction
12//!   boundaries belong in application/domain services.
13//! - **Domain services** may use `TxConfig` to specify transaction requirements.
14//!
15//! # Example
16//!
17//! ```ignore
18//! use modkit_db::secure::{SecureConn, TxConfig, TxIsolationLevel, TxAccessMode};
19//!
20//! // In a domain service:
21//! pub async fn transfer_funds(
22//!     db: &SecureConn,
23//!     from: Uuid,
24//!     to: Uuid,
25//!     amount: Decimal,
26//! ) -> anyhow::Result<()> {
27//!     let cfg = TxConfig {
28//!         isolation: Some(TxIsolationLevel::Serializable),
29//!         access_mode: Some(TxAccessMode::ReadWrite),
30//!     };
31//!
32//!     db.transaction_with_config(cfg, |tx| async move {
33//!         accounts_repo.debit(from, amount, tx).await?;
34//!         accounts_repo.credit(to, amount, tx).await?;
35//!         Ok(())
36//!     }).await
37//! }
38//! ```
39
40/// Transaction isolation level.
41///
42/// Controls how transaction integrity is maintained when multiple transactions
43/// access the same data concurrently.
44///
45/// # Variants
46///
47/// - `ReadUncommitted`: Lowest isolation. Allows dirty reads.
48/// - `ReadCommitted`: Prevents dirty reads. Default for most databases.
49/// - `RepeatableRead`: Prevents dirty reads and non-repeatable reads.
50/// - `Serializable`: Highest isolation. Transactions are fully serialized.
51///
52/// # Backend Notes
53///
54/// - **`PostgreSQL`**: Supports all levels. `RepeatableRead` actually uses
55///   snapshot isolation.
56/// - **MySQL/InnoDB**: Supports all levels.
57/// - **`SQLite`**: Only supports `Serializable` (the default). Other levels
58///   are mapped to `Serializable`.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum TxIsolationLevel {
61    /// Allows dirty reads. Not recommended for most use cases.
62    ReadUncommitted,
63    /// Prevents dirty reads. This is the default for most databases.
64    #[default]
65    ReadCommitted,
66    /// Prevents dirty reads and non-repeatable reads.
67    RepeatableRead,
68    /// Full serialization of transactions. Highest isolation level.
69    Serializable,
70}
71
72/// Transaction access mode.
73///
74/// Specifies whether the transaction will modify data or only read it.
75///
76/// # Variants
77///
78/// - `ReadOnly`: Transaction will not modify data. May enable optimizations.
79/// - `ReadWrite`: Transaction may modify data (default).
80///
81/// # Backend Notes
82///
83/// - **`PostgreSQL`**: `READ ONLY` transactions reject any write operations.
84/// - **`MySQL`**: Supports `READ ONLY` mode for `InnoDB`.
85/// - **`SQLite`**: Read-only mode is not explicitly supported; this is a hint.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
87pub enum TxAccessMode {
88    /// Transaction will only read data.
89    ReadOnly,
90    /// Transaction may read and write data (default).
91    #[default]
92    ReadWrite,
93}
94
95/// Configuration for database transactions.
96///
97/// Use this struct to specify transaction isolation level and access mode
98/// without importing `SeaORM` types.
99///
100/// # Example
101///
102/// ```ignore
103/// use modkit_db::secure::{TxConfig, TxIsolationLevel, TxAccessMode};
104///
105/// // Default configuration (database defaults)
106/// let default_cfg = TxConfig::default();
107///
108/// // Explicit configuration
109/// let cfg = TxConfig {
110///     isolation: Some(TxIsolationLevel::RepeatableRead),
111///     access_mode: Some(TxAccessMode::ReadOnly),
112/// };
113/// ```
114#[derive(Debug, Clone, Default)]
115pub struct TxConfig {
116    /// Transaction isolation level. If `None`, uses database default.
117    pub isolation: Option<TxIsolationLevel>,
118    /// Transaction access mode. If `None`, uses database default (usually `ReadWrite`).
119    pub access_mode: Option<TxAccessMode>,
120}
121
122impl TxConfig {
123    /// Create a new configuration with the specified isolation level.
124    #[must_use]
125    pub fn with_isolation(isolation: TxIsolationLevel) -> Self {
126        Self {
127            isolation: Some(isolation),
128            access_mode: None,
129        }
130    }
131
132    /// Create a read-only transaction configuration.
133    #[must_use]
134    pub fn read_only() -> Self {
135        Self {
136            isolation: None,
137            access_mode: Some(TxAccessMode::ReadOnly),
138        }
139    }
140
141    /// Create a serializable transaction configuration.
142    ///
143    /// This is the highest isolation level, ensuring full serialization
144    /// of transactions.
145    #[must_use]
146    pub fn serializable() -> Self {
147        Self {
148            isolation: Some(TxIsolationLevel::Serializable),
149            access_mode: None,
150        }
151    }
152}
153
154// ============================================================================
155// SeaORM conversions (internal to modkit-db)
156// ============================================================================
157
158use sea_orm::{AccessMode, IsolationLevel};
159
160impl From<TxIsolationLevel> for IsolationLevel {
161    fn from(level: TxIsolationLevel) -> Self {
162        match level {
163            TxIsolationLevel::ReadUncommitted => IsolationLevel::ReadUncommitted,
164            TxIsolationLevel::ReadCommitted => IsolationLevel::ReadCommitted,
165            TxIsolationLevel::RepeatableRead => IsolationLevel::RepeatableRead,
166            TxIsolationLevel::Serializable => IsolationLevel::Serializable,
167        }
168    }
169}
170
171impl From<TxAccessMode> for AccessMode {
172    fn from(mode: TxAccessMode) -> Self {
173        match mode {
174            TxAccessMode::ReadOnly => AccessMode::ReadOnly,
175            TxAccessMode::ReadWrite => AccessMode::ReadWrite,
176        }
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_default_tx_config() {
186        let cfg = TxConfig::default();
187        assert!(cfg.isolation.is_none());
188        assert!(cfg.access_mode.is_none());
189    }
190
191    #[test]
192    fn test_tx_config_with_isolation() {
193        let cfg = TxConfig::with_isolation(TxIsolationLevel::Serializable);
194        assert_eq!(cfg.isolation, Some(TxIsolationLevel::Serializable));
195        assert!(cfg.access_mode.is_none());
196    }
197
198    #[test]
199    fn test_tx_config_read_only() {
200        let cfg = TxConfig::read_only();
201        assert!(cfg.isolation.is_none());
202        assert_eq!(cfg.access_mode, Some(TxAccessMode::ReadOnly));
203    }
204
205    #[test]
206    fn test_tx_config_serializable() {
207        let cfg = TxConfig::serializable();
208        assert_eq!(cfg.isolation, Some(TxIsolationLevel::Serializable));
209        assert!(cfg.access_mode.is_none());
210    }
211
212    #[test]
213    fn test_isolation_level_conversion() {
214        assert!(matches!(
215            IsolationLevel::from(TxIsolationLevel::ReadUncommitted),
216            IsolationLevel::ReadUncommitted
217        ));
218        assert!(matches!(
219            IsolationLevel::from(TxIsolationLevel::ReadCommitted),
220            IsolationLevel::ReadCommitted
221        ));
222        assert!(matches!(
223            IsolationLevel::from(TxIsolationLevel::RepeatableRead),
224            IsolationLevel::RepeatableRead
225        ));
226        assert!(matches!(
227            IsolationLevel::from(TxIsolationLevel::Serializable),
228            IsolationLevel::Serializable
229        ));
230    }
231
232    #[test]
233    fn test_access_mode_conversion() {
234        assert!(matches!(
235            AccessMode::from(TxAccessMode::ReadOnly),
236            AccessMode::ReadOnly
237        ));
238        assert!(matches!(
239            AccessMode::from(TxAccessMode::ReadWrite),
240            AccessMode::ReadWrite
241        ));
242    }
243}