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}