1use core::marker::PhantomData;
4
5use crate::common::PostgresTransactionType;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum IsolationLevel {
10 ReadUncommitted,
12 ReadCommitted,
14 RepeatableRead,
16 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum AccessMode {
34 ReadOnly,
36 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#[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 #[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 pub const fn builder() -> ConfigBuilder {
87 ConfigBuilder::new()
88 }
89
90 #[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 #[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 #[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 #[must_use]
127 pub const fn isolation(&self) -> Option<IsolationLevel> {
128 self.isolation_level
129 }
130
131 #[must_use]
133 pub const fn access(&self) -> Option<AccessMode> {
134 self.access_mode
135 }
136
137 #[must_use]
139 pub const fn is_deferrable(&self) -> bool {
140 self.deferrable
141 }
142
143 #[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#[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 pub const fn isolation_level(
213 self,
214 level: IsolationLevel,
215 ) -> ConfigBuilder<state::Dynamic, Access> {
216 self.isolation(level)
217 }
218
219 pub const fn read_uncommitted(self) -> ConfigBuilder<state::ReadUncommitted, Access> {
221 self.isolation(IsolationLevel::ReadUncommitted)
222 }
223
224 pub const fn read_committed(self) -> ConfigBuilder<state::ReadCommitted, Access> {
226 self.isolation(IsolationLevel::ReadCommitted)
227 }
228
229 pub const fn repeatable_read(self) -> ConfigBuilder<state::RepeatableRead, Access> {
231 self.isolation(IsolationLevel::RepeatableRead)
232 }
233
234 pub const fn serializable(self) -> ConfigBuilder<state::Serializable, Access> {
236 self.isolation(IsolationLevel::Serializable)
237 }
238
239 pub const fn access_mode(self, mode: AccessMode) -> ConfigBuilder<Isolation, state::Dynamic> {
241 self.access(mode)
242 }
243
244 pub const fn read_only(self) -> ConfigBuilder<Isolation, state::ReadOnly> {
246 self.access(AccessMode::ReadOnly)
247 }
248
249 pub const fn read_write(self) -> ConfigBuilder<Isolation, state::ReadWrite> {
251 self.access(AccessMode::ReadWrite)
252 }
253
254 #[must_use]
256 pub const fn build(self) -> TransactionConfig {
257 self.config
258 }
259}
260
261impl ConfigBuilder<state::Serializable, state::ReadOnly> {
262 pub const fn deferrable(mut self) -> Self {
265 self.config.deferrable = true;
266 self
267 }
268}
269
270#[doc(hidden)]
272pub mod state {
273 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
275 pub struct ServerDefault;
276 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
278 pub struct Dynamic;
279 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
281 pub struct ReadUncommitted;
282 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
284 pub struct ReadCommitted;
285 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
287 pub struct RepeatableRead;
288 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
290 pub struct Serializable;
291 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
293 pub struct ReadOnly;
294 #[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}