use core::marker::PhantomData;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IsolationLevel {
ReadUncommitted,
ReadCommitted,
RepeatableRead,
Serializable,
}
impl core::fmt::Display for IsolationLevel {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str(match self {
Self::ReadUncommitted => "READ UNCOMMITTED",
Self::ReadCommitted => "READ COMMITTED",
Self::RepeatableRead => "REPEATABLE READ",
Self::Serializable => "SERIALIZABLE",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessMode {
ReadOnly,
ReadWrite,
}
impl core::fmt::Display for AccessMode {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str(match self {
Self::ReadOnly => "READ ONLY",
Self::ReadWrite => "READ WRITE",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TransactionConfig {
isolation_level: Option<IsolationLevel>,
access_mode: Option<AccessMode>,
consistent_snapshot: bool,
}
impl TransactionConfig {
#[must_use]
pub const fn new() -> Self {
Self {
isolation_level: None,
access_mode: None,
consistent_snapshot: false,
}
}
pub const fn builder() -> ConfigBuilder {
ConfigBuilder::new()
}
#[must_use]
pub const fn isolation_level(mut self, level: IsolationLevel) -> Self {
self.isolation_level = Some(level);
if !matches!(level, IsolationLevel::RepeatableRead) {
self.consistent_snapshot = false;
}
self
}
#[must_use]
pub const fn access_mode(mut self, mode: AccessMode) -> Self {
self.access_mode = Some(mode);
self
}
#[must_use]
pub const fn with_consistent_snapshot(mut self) -> Self {
self.isolation_level = Some(IsolationLevel::RepeatableRead);
self.consistent_snapshot = true;
self
}
#[must_use]
pub const fn isolation(&self) -> Option<IsolationLevel> {
self.isolation_level
}
#[must_use]
pub const fn access(&self) -> Option<AccessMode> {
self.access_mode
}
#[must_use]
pub const fn consistent_snapshot(&self) -> bool {
self.consistent_snapshot
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[must_use]
pub struct ConfigBuilder<Isolation = state::ServerDefault> {
config: TransactionConfig,
state: PhantomData<Isolation>,
}
impl ConfigBuilder {
const fn new() -> Self {
Self {
config: TransactionConfig::new(),
state: PhantomData,
}
}
}
impl<Isolation> ConfigBuilder<Isolation> {
const fn isolation<Next>(mut self, level: IsolationLevel) -> ConfigBuilder<Next> {
self.config.isolation_level = Some(level);
if !matches!(level, IsolationLevel::RepeatableRead) {
self.config.consistent_snapshot = false;
}
ConfigBuilder {
config: self.config,
state: PhantomData,
}
}
pub const fn isolation_level(self, level: IsolationLevel) -> ConfigBuilder<state::Dynamic> {
self.isolation(level)
}
pub const fn read_uncommitted(self) -> ConfigBuilder<state::ReadUncommitted> {
self.isolation(IsolationLevel::ReadUncommitted)
}
pub const fn read_committed(self) -> ConfigBuilder<state::ReadCommitted> {
self.isolation(IsolationLevel::ReadCommitted)
}
pub const fn repeatable_read(self) -> ConfigBuilder<state::RepeatableRead> {
self.isolation(IsolationLevel::RepeatableRead)
}
pub const fn serializable(self) -> ConfigBuilder<state::Serializable> {
self.isolation(IsolationLevel::Serializable)
}
pub const fn read_only(mut self) -> Self {
self.config.access_mode = Some(AccessMode::ReadOnly);
self
}
pub const fn read_write(mut self) -> Self {
self.config.access_mode = Some(AccessMode::ReadWrite);
self
}
#[must_use]
pub const fn build(self) -> TransactionConfig {
self.config
}
}
impl ConfigBuilder<state::RepeatableRead> {
pub const fn snapshot(mut self) -> Self {
self.config.consistent_snapshot = true;
self
}
}
#[doc(hidden)]
pub mod state {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ServerDefault;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Dynamic;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReadUncommitted;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReadCommitted;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RepeatableRead;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Serializable;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transaction_options_default_to_server_policy() {
assert_eq!(TransactionConfig::new(), TransactionConfig::default());
}
#[test]
fn builder_preserves_selected_options() {
let config = TransactionConfig::builder()
.repeatable_read()
.read_only()
.snapshot()
.build();
assert_eq!(config.isolation(), Some(IsolationLevel::RepeatableRead));
assert_eq!(config.access(), Some(AccessMode::ReadOnly));
assert!(config.consistent_snapshot());
}
#[test]
fn runtime_options_remain_available() {
let config = TransactionConfig::new()
.isolation_level(IsolationLevel::Serializable)
.access_mode(AccessMode::ReadWrite)
.with_consistent_snapshot();
assert_eq!(config.isolation(), Some(IsolationLevel::RepeatableRead));
assert_eq!(config.access(), Some(AccessMode::ReadWrite));
assert!(config.consistent_snapshot());
}
#[test]
fn runtime_isolation_change_clears_snapshot() {
let config = TransactionConfig::new()
.with_consistent_snapshot()
.isolation_level(IsolationLevel::Serializable);
assert_eq!(config.isolation(), Some(IsolationLevel::Serializable));
assert!(!config.consistent_snapshot());
}
#[test]
fn repeated_runtime_isolation_preserves_snapshot() {
let config = TransactionConfig::new()
.with_consistent_snapshot()
.isolation_level(IsolationLevel::RepeatableRead);
assert!(config.consistent_snapshot());
}
#[test]
fn changing_isolation_clears_snapshot() {
let config = TransactionConfig::builder()
.repeatable_read()
.snapshot()
.serializable()
.build();
assert_eq!(config.isolation(), Some(IsolationLevel::Serializable));
assert!(!config.consistent_snapshot());
}
#[test]
fn repeated_builder_isolation_preserves_snapshot() {
let config = TransactionConfig::builder()
.repeatable_read()
.snapshot()
.repeatable_read()
.build();
assert!(config.consistent_snapshot());
}
}