use std::path::PathBuf;
use anyhow::{anyhow, bail, Result};
use doublets::data::LinkReference;
use crate::error::LinkError;
use crate::link::GenericLink;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TransitionKind {
Create,
Update,
Delete,
}
impl TransitionKind {
pub(crate) fn as_u8(self) -> u8 {
match self {
TransitionKind::Create => 0,
TransitionKind::Update => 1,
TransitionKind::Delete => 2,
}
}
pub(crate) fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(TransitionKind::Create),
1 => Some(TransitionKind::Update),
2 => Some(TransitionKind::Delete),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CommitMode {
#[default]
Sync,
Async,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum LogRetentionPolicy {
#[default]
Infinite,
Sized { max_transitions: u64 },
Chunked {
chunk_size: u64,
archive_directory: PathBuf,
},
}
impl LogRetentionPolicy {
pub fn parse(spec: &str) -> Result<Self> {
let trimmed = spec.trim();
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("infinite") {
return Ok(Self::Infinite);
}
let lowered = trimmed.to_ascii_lowercase();
if lowered.starts_with("sized:") {
let rest = &trimmed["sized:".len()..];
let max: u64 = rest
.parse()
.map_err(|_| anyhow!("invalid sized retention spec '{spec}'"))?;
return Ok(Self::Sized {
max_transitions: max,
});
}
if lowered.starts_with("chunked:") {
let rest = &trimmed["chunked:".len()..];
let (size_text, dir) = rest
.split_once(':')
.ok_or_else(|| anyhow!("invalid chunked retention spec '{spec}'"))?;
let chunk_size: u64 = size_text
.parse()
.map_err(|_| anyhow!("invalid chunked size in '{spec}'"))?;
if chunk_size == 0 {
bail!("invalid chunked size in '{spec}'");
}
if dir.is_empty() {
bail!("invalid chunked retention spec '{spec}'");
}
return Ok(Self::Chunked {
chunk_size,
archive_directory: PathBuf::from(dir),
});
}
bail!("unknown retention spec '{spec}'");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct GenericDoubletLink<T> {
pub index: T,
pub source: T,
pub target: T,
}
impl<T> GenericDoubletLink<T> {
pub const fn new(index: T, source: T, target: T) -> Self {
Self {
index,
source,
target,
}
}
}
impl<T: LinkReference> GenericDoubletLink<T> {
pub fn empty() -> Self {
let zero = T::from_byte(0);
Self::new(zero, zero, zero)
}
pub fn from_link(link: &GenericLink<T>) -> Self {
Self::new(link.index, link.source, link.target)
}
fn serialize(&self) -> String {
format!("{},{},{}", self.index, self.source, self.target)
}
fn parse(text: &str) -> Result<Self, LinkError> {
let parts: Vec<&str> = text.split(',').collect();
if parts.len() != 3 {
return Err(LinkError::InvalidFormat(format!(
"expected 'index,source,target' in transition, got '{text}'"
)));
}
Ok(Self::new(
parse_address(parts[0])?,
parse_address(parts[1])?,
parse_address(parts[2])?,
))
}
}
impl<T: LinkReference> From<GenericLink<T>> for GenericDoubletLink<T> {
fn from(link: GenericLink<T>) -> Self {
Self::new(link.index, link.source, link.target)
}
}
impl<T: LinkReference> From<GenericDoubletLink<T>> for GenericLink<T> {
fn from(link: GenericDoubletLink<T>) -> Self {
Self::new(link.index, link.source, link.target)
}
}
pub type DoubletLink = GenericDoubletLink<u32>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GenericTransition<T> {
pub transaction_id: u128,
pub sequence: i64,
pub timestamp_ms: i64,
pub kind: TransitionKind,
pub before: GenericDoubletLink<T>,
pub after: GenericDoubletLink<T>,
}
pub type Transition = GenericTransition<u32>;
impl<T: LinkReference> GenericTransition<T> {
pub(crate) const SCHEMA_VERSION: &'static str = "v1";
pub fn serialize(&self) -> String {
format!(
"{schema}|{tx:032x}|{seq}|{ms}|{kind}|{before}|{after}",
schema = Self::SCHEMA_VERSION,
tx = self.transaction_id,
seq = self.sequence,
ms = self.timestamp_ms,
kind = self.kind.as_u8(),
before = self.before.serialize(),
after = self.after.serialize(),
)
}
pub fn parse(text: &str) -> Result<Self, LinkError> {
let invalid = || LinkError::InvalidFormat(format!("malformed transition entry '{text}'"));
if text.is_empty() {
return Err(invalid());
}
let parts: Vec<&str> = text.split('|').collect();
if parts.len() < 7 || parts[0] != Self::SCHEMA_VERSION {
return Err(invalid());
}
let transaction_id = u128::from_str_radix(parts[1], 16).map_err(|_| invalid())?;
let sequence: i64 = parts[2].parse().map_err(|_| invalid())?;
let timestamp_ms: i64 = parts[3].parse().map_err(|_| invalid())?;
let kind_value: u8 = parts[4].parse().map_err(|_| invalid())?;
let kind = TransitionKind::from_u8(kind_value).ok_or_else(invalid)?;
let before =
GenericDoubletLink::parse(parts[5]).map_err(|error| keep_range(error, &invalid))?;
let after =
GenericDoubletLink::parse(parts[6]).map_err(|error| keep_range(error, &invalid))?;
Ok(Self {
transaction_id,
sequence,
timestamp_ms,
kind,
before,
after,
})
}
pub fn try_parse(text: &str) -> Option<Self> {
Self::parse(text).ok()
}
}
fn keep_range(error: LinkError, invalid: &dyn Fn() -> LinkError) -> LinkError {
match error {
LinkError::AddressOutOfRange(value) => LinkError::AddressOutOfRange(value),
_ => invalid(),
}
}
fn parse_address<T: LinkReference>(text: &str) -> Result<T, LinkError> {
let value: u128 = text
.parse()
.map_err(|_| LinkError::InvalidFormat(format!("invalid link address '{text}'")))?;
T::try_from(value).map_err(|_| LinkError::AddressOutOfRange(value))
}
pub(crate) const COMMIT_MARKER_PREFIX: &str = "__transactions:commit:";
pub(crate) const ROLLBACK_MARKER_PREFIX: &str = "__transactions:rollback:";
pub(crate) const APPLIED_MARKER_PREFIX: &str = "__transactions:applied:";
pub(crate) const TRANSITION_NAME_PREFIX: &str = "__transactions:transition:";