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(Clone, Copy, PartialEq, Eq, Hash)]
58pub struct SnapshotId {
59 bytes: [u8; SnapshotId::CAPACITY],
60 len: u8,
61}
62
63impl SnapshotId {
64 const CAPACITY: usize = 64;
67
68 #[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 #[allow(clippy::cast_possible_truncation)]
89 let len = source.len() as u8;
90 Some(Self { bytes, len })
91 }
92
93 #[must_use]
95 pub fn as_str(&self) -> &str {
96 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#[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 #[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 pub const fn builder() -> ConfigBuilder {
189 ConfigBuilder::new()
190 }
191
192 #[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 #[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 #[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 #[must_use]
240 pub const fn isolation(&self) -> Option<IsolationLevel> {
241 self.isolation_level
242 }
243
244 #[must_use]
246 pub const fn access(&self) -> Option<AccessMode> {
247 self.access_mode
248 }
249
250 #[must_use]
252 pub const fn is_deferrable(&self) -> bool {
253 self.deferrable
254 }
255
256 #[must_use]
258 pub const fn snapshot(&self) -> Option<SnapshotId> {
259 self.snapshot
260 }
261
262 #[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#[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 pub const fn isolation_level(
330 self,
331 level: IsolationLevel,
332 ) -> ConfigBuilder<state::Dynamic, Access> {
333 self.isolation(level)
334 }
335
336 pub const fn read_uncommitted(self) -> ConfigBuilder<state::ReadUncommitted, Access> {
338 self.isolation(IsolationLevel::ReadUncommitted)
339 }
340
341 pub const fn read_committed(self) -> ConfigBuilder<state::ReadCommitted, Access> {
343 self.isolation(IsolationLevel::ReadCommitted)
344 }
345
346 pub const fn repeatable_read(self) -> ConfigBuilder<state::RepeatableRead, Access> {
348 self.isolation(IsolationLevel::RepeatableRead)
349 }
350
351 pub const fn serializable(self) -> ConfigBuilder<state::Serializable, Access> {
353 self.isolation(IsolationLevel::Serializable)
354 }
355
356 pub const fn access_mode(self, mode: AccessMode) -> ConfigBuilder<Isolation, state::Dynamic> {
358 self.access(mode)
359 }
360
361 pub const fn read_only(self) -> ConfigBuilder<Isolation, state::ReadOnly> {
363 self.access(AccessMode::ReadOnly)
364 }
365
366 pub const fn read_write(self) -> ConfigBuilder<Isolation, state::ReadWrite> {
368 self.access(AccessMode::ReadWrite)
369 }
370
371 #[must_use]
373 pub const fn build(self) -> TransactionConfig {
374 self.config
375 }
376}
377
378impl<Access> ConfigBuilder<state::RepeatableRead, Access> {
379 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 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 pub const fn deferrable(mut self) -> Self {
408 self.config.deferrable = true;
409 self
410 }
411}
412
413#[doc(hidden)]
415pub mod state {
416 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
418 pub struct ServerDefault;
419 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
421 pub struct Dynamic;
422 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
424 pub struct ReadUncommitted;
425 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
427 pub struct ReadCommitted;
428 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
430 pub struct RepeatableRead;
431 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
433 pub struct Serializable;
434 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
436 pub struct ReadOnly;
437 #[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}