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)]
89pub struct TransactionConfig {
90 isolation_level: Option<IsolationLevel>,
91 access_mode: Option<AccessMode>,
92 deferrable: bool,
93 legacy_read_committed: bool,
94}
95
96impl TransactionConfig {
97 #[must_use]
99 pub const fn new() -> Self {
100 Self {
101 isolation_level: None,
102 access_mode: None,
103 deferrable: false,
104 legacy_read_committed: false,
105 }
106 }
107
108 pub const fn builder() -> ConfigBuilder {
110 ConfigBuilder::new()
111 }
112
113 #[must_use]
117 pub const fn isolation_level(mut self, level: IsolationLevel) -> Self {
118 self.isolation_level = Some(level);
119 if !matches!(level, IsolationLevel::Serializable) {
120 self.deferrable = false;
121 }
122 self.legacy_read_committed = false;
123 self
124 }
125
126 #[must_use]
130 pub const fn access_mode(mut self, mode: AccessMode) -> Self {
131 self.access_mode = Some(mode);
132 if !matches!(mode, AccessMode::ReadOnly) {
133 self.deferrable = false;
134 }
135 self
136 }
137
138 #[must_use]
144 pub const fn deferrable(mut self) -> Self {
145 self.isolation_level = Some(IsolationLevel::Serializable);
146 self.access_mode = Some(AccessMode::ReadOnly);
147 self.deferrable = true;
148 self.legacy_read_committed = false;
149 self
150 }
151
152 #[must_use]
154 pub const fn isolation(&self) -> Option<IsolationLevel> {
155 self.isolation_level
156 }
157
158 #[must_use]
160 pub const fn access(&self) -> Option<AccessMode> {
161 self.access_mode
162 }
163
164 #[must_use]
166 pub const fn is_deferrable(&self) -> bool {
167 self.deferrable
168 }
169
170 #[doc(hidden)]
173 #[must_use]
174 pub const fn uses_server_default_isolation(&self) -> bool {
175 self.isolation_level.is_none() || self.legacy_read_committed
176 }
177}
178
179impl From<PostgresTransactionType> for TransactionConfig {
180 fn from(tx_type: PostgresTransactionType) -> Self {
181 let isolation_level = match tx_type {
182 PostgresTransactionType::ReadCommitted => Some(IsolationLevel::ReadCommitted),
183 PostgresTransactionType::ReadUncommitted => Some(IsolationLevel::ReadUncommitted),
184 PostgresTransactionType::RepeatableRead => Some(IsolationLevel::RepeatableRead),
185 PostgresTransactionType::Serializable => Some(IsolationLevel::Serializable),
186 };
187 Self {
188 isolation_level,
189 legacy_read_committed: matches!(tx_type, PostgresTransactionType::ReadCommitted),
190 ..Self::new()
191 }
192 }
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201#[must_use]
202pub struct ConfigBuilder<Isolation = state::ServerDefault, Access = state::ServerDefault> {
203 config: TransactionConfig,
204 state: PhantomData<(Isolation, Access)>,
205}
206
207impl ConfigBuilder {
208 const fn new() -> Self {
209 Self {
210 config: TransactionConfig::new(),
211 state: PhantomData,
212 }
213 }
214}
215
216impl<Isolation, Access> ConfigBuilder<Isolation, Access> {
217 const fn isolation<Next>(mut self, level: IsolationLevel) -> ConfigBuilder<Next, Access> {
218 self.config.isolation_level = Some(level);
219 if !matches!(level, IsolationLevel::Serializable) {
220 self.config.deferrable = false;
221 }
222 self.config.legacy_read_committed = false;
223 ConfigBuilder {
224 config: self.config,
225 state: PhantomData,
226 }
227 }
228
229 const fn access<Next>(mut self, mode: AccessMode) -> ConfigBuilder<Isolation, Next> {
230 self.config.access_mode = Some(mode);
231 if !matches!(mode, AccessMode::ReadOnly) {
232 self.config.deferrable = false;
233 }
234 ConfigBuilder {
235 config: self.config,
236 state: PhantomData,
237 }
238 }
239
240 pub const fn isolation_level(
242 self,
243 level: IsolationLevel,
244 ) -> ConfigBuilder<state::Dynamic, Access> {
245 self.isolation(level)
246 }
247
248 pub const fn read_uncommitted(self) -> ConfigBuilder<state::ReadUncommitted, Access> {
250 self.isolation(IsolationLevel::ReadUncommitted)
251 }
252
253 pub const fn read_committed(self) -> ConfigBuilder<state::ReadCommitted, Access> {
255 self.isolation(IsolationLevel::ReadCommitted)
256 }
257
258 pub const fn repeatable_read(self) -> ConfigBuilder<state::RepeatableRead, Access> {
260 self.isolation(IsolationLevel::RepeatableRead)
261 }
262
263 pub const fn serializable(self) -> ConfigBuilder<state::Serializable, Access> {
265 self.isolation(IsolationLevel::Serializable)
266 }
267
268 pub const fn access_mode(self, mode: AccessMode) -> ConfigBuilder<Isolation, state::Dynamic> {
270 self.access(mode)
271 }
272
273 pub const fn read_only(self) -> ConfigBuilder<Isolation, state::ReadOnly> {
275 self.access(AccessMode::ReadOnly)
276 }
277
278 pub const fn read_write(self) -> ConfigBuilder<Isolation, state::ReadWrite> {
280 self.access(AccessMode::ReadWrite)
281 }
282
283 #[must_use]
285 pub const fn build(self) -> TransactionConfig {
286 self.config
287 }
288}
289
290impl ConfigBuilder<state::Serializable, state::ReadOnly> {
291 pub const fn deferrable(mut self) -> Self {
296 self.config.deferrable = true;
297 self
298 }
299}
300
301#[doc(hidden)]
303pub mod state {
304 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
306 pub struct ServerDefault;
307 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
309 pub struct Dynamic;
310 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
312 pub struct ReadUncommitted;
313 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
315 pub struct ReadCommitted;
316 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
318 pub struct RepeatableRead;
319 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
321 pub struct Serializable;
322 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
324 pub struct ReadOnly;
325 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
327 pub struct ReadWrite;
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn default_uses_server_policy() {
336 assert_eq!(TransactionConfig::new(), TransactionConfig::default());
337 }
338
339 #[test]
340 fn builder_preserves_selected_options() {
341 let config = TransactionConfig::builder()
342 .serializable()
343 .read_only()
344 .deferrable()
345 .build();
346
347 assert_eq!(config.isolation(), Some(IsolationLevel::Serializable));
348 assert_eq!(config.access(), Some(AccessMode::ReadOnly));
349 assert!(config.is_deferrable());
350 }
351
352 #[test]
353 fn legacy_read_committed_preserves_adapter_behavior() {
354 let config = TransactionConfig::from(PostgresTransactionType::ReadCommitted);
355 assert_eq!(config.isolation(), Some(IsolationLevel::ReadCommitted));
356 assert!(config.uses_server_default_isolation());
357 }
358
359 #[test]
360 fn changing_access_clears_deferrable() {
361 let config = TransactionConfig::builder()
362 .serializable()
363 .read_only()
364 .deferrable()
365 .read_write()
366 .build();
367
368 assert_eq!(config.access(), Some(AccessMode::ReadWrite));
369 assert!(!config.is_deferrable());
370 }
371
372 #[test]
373 fn runtime_setters_clear_invalid_deferrable_state() {
374 let config = TransactionConfig::builder()
375 .serializable()
376 .read_only()
377 .deferrable()
378 .build()
379 .access_mode(AccessMode::ReadWrite)
380 .isolation_level(IsolationLevel::ReadCommitted);
381
382 assert!(!config.is_deferrable());
383 assert!(!config.uses_server_default_isolation());
384 }
385
386 #[test]
387 fn compatible_runtime_setters_preserve_deferrable() {
388 let config = TransactionConfig::new()
389 .deferrable()
390 .isolation_level(IsolationLevel::Serializable)
391 .access_mode(AccessMode::ReadOnly);
392
393 assert!(config.is_deferrable());
394 }
395
396 #[test]
397 fn compatible_builder_transitions_preserve_deferrable() {
398 let config = TransactionConfig::builder()
399 .serializable()
400 .read_only()
401 .deferrable()
402 .serializable()
403 .read_only()
404 .build();
405
406 assert!(config.is_deferrable());
407 }
408
409 #[test]
410 fn access_mode_preserves_legacy_server_default_isolation() {
411 let config = TransactionConfig::from(PostgresTransactionType::ReadCommitted)
412 .access_mode(AccessMode::ReadOnly);
413
414 assert_eq!(config.isolation(), Some(IsolationLevel::ReadCommitted));
415 assert!(config.uses_server_default_isolation());
416 }
417
418 #[test]
419 fn runtime_deferrable_selects_valid_modes() {
420 let config = TransactionConfig::new().deferrable();
421
422 assert_eq!(config.isolation(), Some(IsolationLevel::Serializable));
423 assert_eq!(config.access(), Some(AccessMode::ReadOnly));
424 assert!(config.is_deferrable());
425 assert!(!config.uses_server_default_isolation());
426 }
427}