use core::marker::PhantomData;
use crate::common::PostgresTransactionType;
#[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>,
deferrable: bool,
legacy_read_committed: bool,
}
impl TransactionConfig {
#[must_use]
pub const fn new() -> Self {
Self {
isolation_level: None,
access_mode: None,
deferrable: false,
legacy_read_committed: 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::Serializable) {
self.deferrable = false;
}
self.legacy_read_committed = false;
self
}
#[must_use]
pub const fn access_mode(mut self, mode: AccessMode) -> Self {
self.access_mode = Some(mode);
if !matches!(mode, AccessMode::ReadOnly) {
self.deferrable = false;
}
self
}
#[must_use]
pub const fn deferrable(mut self) -> Self {
self.isolation_level = Some(IsolationLevel::Serializable);
self.access_mode = Some(AccessMode::ReadOnly);
self.deferrable = true;
self.legacy_read_committed = false;
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 is_deferrable(&self) -> bool {
self.deferrable
}
#[doc(hidden)]
#[must_use]
pub const fn uses_server_default_isolation(&self) -> bool {
self.isolation_level.is_none() || self.legacy_read_committed
}
}
impl From<PostgresTransactionType> for TransactionConfig {
fn from(tx_type: PostgresTransactionType) -> Self {
let isolation_level = match tx_type {
PostgresTransactionType::ReadCommitted => Some(IsolationLevel::ReadCommitted),
PostgresTransactionType::ReadUncommitted => Some(IsolationLevel::ReadUncommitted),
PostgresTransactionType::RepeatableRead => Some(IsolationLevel::RepeatableRead),
PostgresTransactionType::Serializable => Some(IsolationLevel::Serializable),
};
Self {
isolation_level,
legacy_read_committed: matches!(tx_type, PostgresTransactionType::ReadCommitted),
..Self::new()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[must_use]
pub struct ConfigBuilder<Isolation = state::ServerDefault, Access = state::ServerDefault> {
config: TransactionConfig,
state: PhantomData<(Isolation, Access)>,
}
impl ConfigBuilder {
const fn new() -> Self {
Self {
config: TransactionConfig::new(),
state: PhantomData,
}
}
}
impl<Isolation, Access> ConfigBuilder<Isolation, Access> {
const fn isolation<Next>(mut self, level: IsolationLevel) -> ConfigBuilder<Next, Access> {
self.config.isolation_level = Some(level);
if !matches!(level, IsolationLevel::Serializable) {
self.config.deferrable = false;
}
self.config.legacy_read_committed = false;
ConfigBuilder {
config: self.config,
state: PhantomData,
}
}
const fn access<Next>(mut self, mode: AccessMode) -> ConfigBuilder<Isolation, Next> {
self.config.access_mode = Some(mode);
if !matches!(mode, AccessMode::ReadOnly) {
self.config.deferrable = false;
}
ConfigBuilder {
config: self.config,
state: PhantomData,
}
}
pub const fn isolation_level(
self,
level: IsolationLevel,
) -> ConfigBuilder<state::Dynamic, Access> {
self.isolation(level)
}
pub const fn read_uncommitted(self) -> ConfigBuilder<state::ReadUncommitted, Access> {
self.isolation(IsolationLevel::ReadUncommitted)
}
pub const fn read_committed(self) -> ConfigBuilder<state::ReadCommitted, Access> {
self.isolation(IsolationLevel::ReadCommitted)
}
pub const fn repeatable_read(self) -> ConfigBuilder<state::RepeatableRead, Access> {
self.isolation(IsolationLevel::RepeatableRead)
}
pub const fn serializable(self) -> ConfigBuilder<state::Serializable, Access> {
self.isolation(IsolationLevel::Serializable)
}
pub const fn access_mode(self, mode: AccessMode) -> ConfigBuilder<Isolation, state::Dynamic> {
self.access(mode)
}
pub const fn read_only(self) -> ConfigBuilder<Isolation, state::ReadOnly> {
self.access(AccessMode::ReadOnly)
}
pub const fn read_write(self) -> ConfigBuilder<Isolation, state::ReadWrite> {
self.access(AccessMode::ReadWrite)
}
#[must_use]
pub const fn build(self) -> TransactionConfig {
self.config
}
}
impl ConfigBuilder<state::Serializable, state::ReadOnly> {
pub const fn deferrable(mut self) -> Self {
self.config.deferrable = 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;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReadOnly;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReadWrite;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_uses_server_policy() {
assert_eq!(TransactionConfig::new(), TransactionConfig::default());
}
#[test]
fn builder_preserves_selected_options() {
let config = TransactionConfig::builder()
.serializable()
.read_only()
.deferrable()
.build();
assert_eq!(config.isolation(), Some(IsolationLevel::Serializable));
assert_eq!(config.access(), Some(AccessMode::ReadOnly));
assert!(config.is_deferrable());
}
#[test]
fn legacy_read_committed_preserves_adapter_behavior() {
let config = TransactionConfig::from(PostgresTransactionType::ReadCommitted);
assert_eq!(config.isolation(), Some(IsolationLevel::ReadCommitted));
assert!(config.uses_server_default_isolation());
}
#[test]
fn changing_access_clears_deferrable() {
let config = TransactionConfig::builder()
.serializable()
.read_only()
.deferrable()
.read_write()
.build();
assert_eq!(config.access(), Some(AccessMode::ReadWrite));
assert!(!config.is_deferrable());
}
#[test]
fn runtime_setters_clear_invalid_deferrable_state() {
let config = TransactionConfig::builder()
.serializable()
.read_only()
.deferrable()
.build()
.access_mode(AccessMode::ReadWrite)
.isolation_level(IsolationLevel::ReadCommitted);
assert!(!config.is_deferrable());
assert!(!config.uses_server_default_isolation());
}
#[test]
fn compatible_runtime_setters_preserve_deferrable() {
let config = TransactionConfig::new()
.deferrable()
.isolation_level(IsolationLevel::Serializable)
.access_mode(AccessMode::ReadOnly);
assert!(config.is_deferrable());
}
#[test]
fn compatible_builder_transitions_preserve_deferrable() {
let config = TransactionConfig::builder()
.serializable()
.read_only()
.deferrable()
.serializable()
.read_only()
.build();
assert!(config.is_deferrable());
}
#[test]
fn access_mode_preserves_legacy_server_default_isolation() {
let config = TransactionConfig::from(PostgresTransactionType::ReadCommitted)
.access_mode(AccessMode::ReadOnly);
assert_eq!(config.isolation(), Some(IsolationLevel::ReadCommitted));
assert!(config.uses_server_default_isolation());
}
#[test]
fn runtime_deferrable_selects_valid_modes() {
let config = TransactionConfig::new().deferrable();
assert_eq!(config.isolation(), Some(IsolationLevel::Serializable));
assert_eq!(config.access(), Some(AccessMode::ReadOnly));
assert!(config.is_deferrable());
assert!(!config.uses_server_default_isolation());
}
}