1use std::path::PathBuf;
9
10use anyhow::{anyhow, bail, Result};
11use doublets::data::LinkReference;
12
13use crate::error::LinkError;
14use crate::link::GenericLink;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum TransitionKind {
19 Create,
20 Update,
21 Delete,
22}
23
24impl TransitionKind {
25 pub fn as_u8(self) -> u8 {
26 match self {
27 TransitionKind::Create => 0,
28 TransitionKind::Update => 1,
29 TransitionKind::Delete => 2,
30 }
31 }
32
33 pub fn from_u8(value: u8) -> Option<Self> {
34 match value {
35 0 => Some(TransitionKind::Create),
36 1 => Some(TransitionKind::Update),
37 2 => Some(TransitionKind::Delete),
38 _ => None,
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
53pub enum CommitMode {
54 #[default]
55 Sync,
56 Async,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Default)]
61pub enum LogRetentionPolicy {
62 #[default]
64 Infinite,
65 Sized { max_transitions: u64 },
68 Chunked {
72 chunk_size: u64,
73 archive_directory: PathBuf,
74 },
75}
76
77impl LogRetentionPolicy {
78 pub fn parse(spec: &str) -> Result<Self> {
80 let trimmed = spec.trim();
81 if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("infinite") {
82 return Ok(Self::Infinite);
83 }
84
85 let lowered = trimmed.to_ascii_lowercase();
86 if lowered.starts_with("sized:") {
87 let rest = &trimmed["sized:".len()..];
88 let max: u64 = rest
89 .parse()
90 .map_err(|_| anyhow!("invalid sized retention spec '{spec}'"))?;
91 return Ok(Self::Sized {
92 max_transitions: max,
93 });
94 }
95 if lowered.starts_with("chunked:") {
96 let rest = &trimmed["chunked:".len()..];
97 let (size_text, dir) = rest
98 .split_once(':')
99 .ok_or_else(|| anyhow!("invalid chunked retention spec '{spec}'"))?;
100 let chunk_size: u64 = size_text
101 .parse()
102 .map_err(|_| anyhow!("invalid chunked size in '{spec}'"))?;
103 if chunk_size == 0 {
104 bail!("invalid chunked size in '{spec}'");
105 }
106 if dir.is_empty() {
107 bail!("invalid chunked retention spec '{spec}'");
108 }
109 return Ok(Self::Chunked {
110 chunk_size,
111 archive_directory: PathBuf::from(dir),
112 });
113 }
114 bail!("unknown retention spec '{spec}'");
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
121pub struct GenericDoubletLink<T> {
122 pub index: T,
123 pub source: T,
124 pub target: T,
125}
126
127impl<T> GenericDoubletLink<T> {
128 pub const fn new(index: T, source: T, target: T) -> Self {
129 Self {
130 index,
131 source,
132 target,
133 }
134 }
135}
136
137impl<T: LinkReference> GenericDoubletLink<T> {
138 pub fn empty() -> Self {
140 let zero = T::from_byte(0);
141 Self::new(zero, zero, zero)
142 }
143
144 pub fn from_link(link: &GenericLink<T>) -> Self {
145 Self::new(link.index, link.source, link.target)
146 }
147
148 fn serialize(&self) -> String {
149 format!("{},{},{}", self.index, self.source, self.target)
150 }
151
152 fn parse(text: &str) -> Result<Self, LinkError> {
153 let parts: Vec<&str> = text.split(',').collect();
154 if parts.len() != 3 {
155 return Err(LinkError::InvalidFormat(format!(
156 "expected 'index,source,target' in transition, got '{text}'"
157 )));
158 }
159 Ok(Self::new(
160 parse_address(parts[0])?,
161 parse_address(parts[1])?,
162 parse_address(parts[2])?,
163 ))
164 }
165}
166
167impl<T: LinkReference> From<GenericLink<T>> for GenericDoubletLink<T> {
168 fn from(link: GenericLink<T>) -> Self {
169 Self::new(link.index, link.source, link.target)
170 }
171}
172
173impl<T: LinkReference> From<GenericDoubletLink<T>> for GenericLink<T> {
174 fn from(link: GenericDoubletLink<T>) -> Self {
175 Self::new(link.index, link.source, link.target)
176 }
177}
178
179pub type DoubletLink = GenericDoubletLink<u32>;
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
186pub struct GenericTransition<T> {
187 pub transaction_id: u128,
188 pub sequence: i64,
189 pub timestamp_ms: i64,
190 pub kind: TransitionKind,
191 pub before: GenericDoubletLink<T>,
192 pub after: GenericDoubletLink<T>,
193}
194
195pub type Transition = GenericTransition<u32>;
197
198impl<T: LinkReference> GenericTransition<T> {
199 pub const SCHEMA_VERSION: &'static str = "v1";
200
201 pub fn serialize(&self) -> String {
208 format!(
209 "{schema}|{tx:032x}|{seq}|{ms}|{kind}|{before}|{after}",
210 schema = Self::SCHEMA_VERSION,
211 tx = self.transaction_id,
212 seq = self.sequence,
213 ms = self.timestamp_ms,
214 kind = self.kind.as_u8(),
215 before = self.before.serialize(),
216 after = self.after.serialize(),
217 )
218 }
219
220 pub fn parse(text: &str) -> Result<Self, LinkError> {
230 let invalid = || LinkError::InvalidFormat(format!("malformed transition entry '{text}'"));
231 if text.is_empty() {
232 return Err(invalid());
233 }
234 let parts: Vec<&str> = text.split('|').collect();
235 if parts.len() < 7 || parts[0] != Self::SCHEMA_VERSION {
236 return Err(invalid());
237 }
238 let transaction_id = u128::from_str_radix(parts[1], 16).map_err(|_| invalid())?;
239 let sequence: i64 = parts[2].parse().map_err(|_| invalid())?;
240 let timestamp_ms: i64 = parts[3].parse().map_err(|_| invalid())?;
241 let kind_value: u8 = parts[4].parse().map_err(|_| invalid())?;
242 let kind = TransitionKind::from_u8(kind_value).ok_or_else(invalid)?;
243 let before =
244 GenericDoubletLink::parse(parts[5]).map_err(|error| keep_range(error, &invalid))?;
245 let after =
246 GenericDoubletLink::parse(parts[6]).map_err(|error| keep_range(error, &invalid))?;
247 Ok(Self {
248 transaction_id,
249 sequence,
250 timestamp_ms,
251 kind,
252 before,
253 after,
254 })
255 }
256
257 pub fn try_parse(text: &str) -> Option<Self> {
259 Self::parse(text).ok()
260 }
261}
262
263fn keep_range(error: LinkError, invalid: &dyn Fn() -> LinkError) -> LinkError {
266 match error {
267 LinkError::AddressOutOfRange(value) => LinkError::AddressOutOfRange(value),
268 _ => invalid(),
269 }
270}
271
272fn parse_address<T: LinkReference>(text: &str) -> Result<T, LinkError> {
274 let value: u128 = text
275 .parse()
276 .map_err(|_| LinkError::InvalidFormat(format!("invalid link address '{text}'")))?;
277 T::try_from(value).map_err(|_| LinkError::AddressOutOfRange(value))
278}
279
280pub const COMMIT_MARKER_PREFIX: &str = "__transactions:commit:";
282pub const ROLLBACK_MARKER_PREFIX: &str = "__transactions:rollback:";
283pub const APPLIED_MARKER_PREFIX: &str = "__transactions:applied:";
284pub const TRANSITION_NAME_PREFIX: &str = "__transactions:transition:";