formualizer_eval/engine/graph/editor/
transaction_manager.rs1use std::sync::atomic::{AtomicU64, Ordering};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct TransactionId(u64);
13
14impl Default for TransactionId {
15 fn default() -> Self {
16 Self::new()
17 }
18}
19
20impl TransactionId {
21 pub fn new() -> Self {
23 static COUNTER: AtomicU64 = AtomicU64::new(0);
24 TransactionId(COUNTER.fetch_add(1, Ordering::Relaxed))
25 }
26}
27
28impl std::fmt::Display for TransactionId {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 write!(f, "tx:{}", self.0)
31 }
32}
33
34#[derive(Debug)]
36struct Transaction {
37 id: TransactionId,
38 start_index: usize,
40 savepoints: Vec<(String, usize)>,
42}
43
44#[derive(Debug, Clone)]
46pub enum TransactionError {
47 AlreadyActive,
49 NoActiveTransaction,
51 TransactionTooLarge { size: usize, max: usize },
53 RollbackFailed(String),
55 SavepointNotFound(String),
57}
58
59impl std::fmt::Display for TransactionError {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 Self::AlreadyActive => write!(f, "Transaction already active"),
63 Self::NoActiveTransaction => write!(f, "No active transaction"),
64 Self::TransactionTooLarge { size, max } => {
65 write!(f, "Transaction too large: {size} > {max}")
66 }
67 Self::RollbackFailed(msg) => write!(f, "Rollback failed: {msg}"),
68 Self::SavepointNotFound(name) => write!(f, "Savepoint not found: {name}"),
69 }
70 }
71}
72
73impl std::error::Error for TransactionError {}
74
75#[derive(Debug)]
77pub struct TransactionManager {
78 active_transaction: Option<Transaction>,
79 max_transaction_size: usize,
81}
82
83impl TransactionManager {
84 pub fn new() -> Self {
86 Self {
87 active_transaction: None,
88 max_transaction_size: 10_000, }
90 }
91
92 pub fn with_max_size(max_size: usize) -> Self {
94 Self {
95 active_transaction: None,
96 max_transaction_size: max_size,
97 }
98 }
99
100 pub fn begin(&mut self, change_log_size: usize) -> Result<TransactionId, TransactionError> {
111 if self.active_transaction.is_some() {
112 return Err(TransactionError::AlreadyActive);
113 }
114
115 let id = TransactionId::new();
116 self.active_transaction = Some(Transaction {
117 id,
118 start_index: change_log_size,
119 savepoints: Vec::new(),
120 });
121 Ok(id)
122 }
123
124 pub fn commit(&mut self) -> Result<TransactionId, TransactionError> {
132 self.active_transaction
133 .take()
134 .map(|tx| tx.id)
135 .ok_or(TransactionError::NoActiveTransaction)
136 }
137
138 pub fn rollback_info(&mut self) -> Result<(TransactionId, usize), TransactionError> {
146 self.active_transaction
147 .take()
148 .map(|tx| (tx.id, tx.start_index))
149 .ok_or(TransactionError::NoActiveTransaction)
150 }
151
152 pub fn add_savepoint(
161 &mut self,
162 name: String,
163 change_log_size: usize,
164 ) -> Result<(), TransactionError> {
165 if let Some(tx) = &mut self.active_transaction {
166 tx.savepoints.push((name, change_log_size));
167 Ok(())
168 } else {
169 Err(TransactionError::NoActiveTransaction)
170 }
171 }
172
173 pub fn get_savepoint(&self, name: &str) -> Result<usize, TransactionError> {
185 if let Some(tx) = &self.active_transaction {
186 tx.savepoints
187 .iter()
188 .find(|(n, _)| n == name)
189 .map(|(_, idx)| *idx)
190 .ok_or_else(|| TransactionError::SavepointNotFound(name.to_string()))
191 } else {
192 Err(TransactionError::NoActiveTransaction)
193 }
194 }
195
196 pub fn truncate_savepoints(&mut self, index: usize) {
201 if let Some(tx) = &mut self.active_transaction {
202 tx.savepoints.retain(|(_, idx)| *idx < index);
203 }
204 }
205
206 pub fn is_active(&self) -> bool {
208 self.active_transaction.is_some()
209 }
210
211 pub fn active_id(&self) -> Option<TransactionId> {
213 self.active_transaction.as_ref().map(|tx| tx.id)
214 }
215
216 pub fn check_size(&self, change_log_size: usize) -> Result<(), TransactionError> {
224 if let Some(tx) = &self.active_transaction {
225 let tx_size = change_log_size - tx.start_index;
226 if tx_size > self.max_transaction_size {
227 return Err(TransactionError::TransactionTooLarge {
228 size: tx_size,
229 max: self.max_transaction_size,
230 });
231 }
232 }
233 Ok(())
234 }
235
236 pub fn max_size(&self) -> usize {
238 self.max_transaction_size
239 }
240
241 pub fn set_max_size(&mut self, max_size: usize) {
243 self.max_transaction_size = max_size;
244 }
245
246 pub fn start_index(&self) -> Option<usize> {
248 self.active_transaction.as_ref().map(|tx| tx.start_index)
249 }
250
251 pub fn savepoints(&self) -> Vec<(String, usize)> {
253 self.active_transaction
254 .as_ref()
255 .map(|tx| tx.savepoints.clone())
256 .unwrap_or_default()
257 }
258}
259
260impl Default for TransactionManager {
261 fn default() -> Self {
262 Self::new()
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_transaction_id_uniqueness() {
272 let id1 = TransactionId::new();
273 let id2 = TransactionId::new();
274 assert_ne!(id1, id2);
275 }
276
277 #[test]
278 fn test_transaction_manager_lifecycle() {
279 let mut tm = TransactionManager::new();
280
281 assert!(!tm.is_active());
283 assert!(tm.commit().is_err());
284 assert!(tm.rollback_info().is_err());
285
286 let tx_id = tm.begin(0).unwrap();
288 assert!(tm.is_active());
289 assert_eq!(tm.active_id(), Some(tx_id));
290
291 assert!(matches!(tm.begin(0), Err(TransactionError::AlreadyActive)));
293
294 let committed_id = tm.commit().unwrap();
296 assert_eq!(tx_id, committed_id);
297 assert!(!tm.is_active());
298
299 assert!(tm.commit().is_err());
301 }
302
303 #[test]
304 fn test_transaction_rollback_info() {
305 let mut tm = TransactionManager::new();
306
307 let tx_id = tm.begin(42).unwrap();
309
310 let (rollback_id, start_index) = tm.rollback_info().unwrap();
312 assert_eq!(rollback_id, tx_id);
313 assert_eq!(start_index, 42);
314
315 assert!(!tm.is_active());
317 }
318
319 #[test]
320 fn test_transaction_size_limits() {
321 let mut tm = TransactionManager::with_max_size(100);
322
323 tm.begin(0).unwrap();
324
325 assert!(tm.check_size(50).is_ok());
327 assert!(tm.check_size(100).is_ok());
328
329 match tm.check_size(101) {
331 Err(TransactionError::TransactionTooLarge { size, max }) => {
332 assert_eq!(size, 101);
333 assert_eq!(max, 100);
334 }
335 _ => panic!("Expected TransactionTooLarge error"),
336 }
337 }
338
339 #[test]
340 fn test_transaction_savepoints() {
341 let mut tm = TransactionManager::new();
342
343 assert!(tm.add_savepoint("test".to_string(), 10).is_err());
345
346 tm.begin(0).unwrap();
347
348 tm.add_savepoint("before_risky_op".to_string(), 10).unwrap();
350 tm.add_savepoint("after_risky_op".to_string(), 20).unwrap();
351 tm.add_savepoint("final".to_string(), 30).unwrap();
352
353 assert_eq!(tm.get_savepoint("before_risky_op").unwrap(), 10);
355 assert_eq!(tm.get_savepoint("after_risky_op").unwrap(), 20);
356 assert_eq!(tm.get_savepoint("final").unwrap(), 30);
357
358 assert!(matches!(
360 tm.get_savepoint("missing"),
361 Err(TransactionError::SavepointNotFound(_))
362 ));
363
364 let savepoints = tm.savepoints();
366 assert_eq!(savepoints.len(), 3);
367 assert_eq!(savepoints[0].0, "before_risky_op");
368 assert_eq!(savepoints[0].1, 10);
369 }
370
371 #[test]
372 fn test_truncate_savepoints() {
373 let mut tm = TransactionManager::new();
374 tm.begin(0).unwrap();
375
376 tm.add_savepoint("sp1".to_string(), 10).unwrap();
377 tm.add_savepoint("sp2".to_string(), 20).unwrap();
378 tm.add_savepoint("sp3".to_string(), 30).unwrap();
379
380 tm.truncate_savepoints(20);
382
383 let savepoints = tm.savepoints();
385 assert_eq!(savepoints.len(), 1);
386 assert_eq!(savepoints[0].0, "sp1");
387
388 assert!(tm.get_savepoint("sp2").is_err());
390 assert!(tm.get_savepoint("sp3").is_err());
391 }
392
393 #[test]
394 fn test_max_size_configuration() {
395 let mut tm = TransactionManager::new();
396 assert_eq!(tm.max_size(), 10_000);
397
398 tm.set_max_size(500);
399 assert_eq!(tm.max_size(), 500);
400
401 tm.begin(0).unwrap();
403 assert!(tm.check_size(499).is_ok());
404 assert!(tm.check_size(501).is_err());
405 }
406
407 #[test]
408 fn test_start_index_tracking() {
409 let mut tm = TransactionManager::new();
410
411 assert_eq!(tm.start_index(), None);
413
414 tm.begin(123).unwrap();
416 assert_eq!(tm.start_index(), Some(123));
417
418 tm.commit().unwrap();
420 assert_eq!(tm.start_index(), None);
421 }
422
423 #[test]
424 fn test_error_display() {
425 let err = TransactionError::AlreadyActive;
426 assert_eq!(format!("{err}"), "Transaction already active");
427
428 let err = TransactionError::NoActiveTransaction;
429 assert_eq!(format!("{err}"), "No active transaction");
430
431 let err = TransactionError::TransactionTooLarge {
432 size: 150,
433 max: 100,
434 };
435 assert_eq!(format!("{err}"), "Transaction too large: 150 > 100");
436
437 let err = TransactionError::RollbackFailed("test error".to_string());
438 assert_eq!(format!("{err}"), "Rollback failed: test error");
439
440 let err = TransactionError::SavepointNotFound("missing".to_string());
441 assert_eq!(format!("{err}"), "Savepoint not found: missing");
442 }
443
444 #[test]
445 fn test_transaction_id_display() {
446 let id = TransactionId(42);
447 assert_eq!(format!("{id}"), "tx:42");
448 }
449}