1use std::collections::{HashMap, VecDeque};
28use thiserror::Error;
29
30#[derive(Debug, Error, PartialEq, Eq, Clone)]
36pub enum TxError {
37 #[error("transaction {0} not found")]
39 TransactionNotFound(u64),
40
41 #[error("transaction {0} is not active")]
43 TransactionNotActive(u64),
44
45 #[error("transaction {0} has already ended")]
47 TransactionAlreadyEnded(u64),
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
56pub struct TransactionId(pub u64);
57
58impl TransactionId {
59 #[inline]
61 pub fn as_u64(self) -> u64 {
62 self.0
63 }
64}
65
66impl std::fmt::Display for TransactionId {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 write!(f, "Tx({})", self.0)
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum TxOperation {
75 Put {
77 key: String,
78 value: Vec<u8>,
79 prev_value: Option<Vec<u8>>,
80 },
81 Delete { key: String, prev_value: Vec<u8> },
83 BatchPut { entries: Vec<(String, Vec<u8>)> },
85}
86
87impl TxOperation {
88 pub fn key_count(&self) -> usize {
90 match self {
91 TxOperation::Put { .. } => 1,
92 TxOperation::Delete { .. } => 1,
93 TxOperation::BatchPut { entries } => entries.len(),
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100pub enum TransactionStatus {
101 Active,
103 Committed,
105 RolledBack,
107 Aborted,
109}
110
111impl std::fmt::Display for TransactionStatus {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 match self {
114 TransactionStatus::Active => write!(f, "Active"),
115 TransactionStatus::Committed => write!(f, "Committed"),
116 TransactionStatus::RolledBack => write!(f, "RolledBack"),
117 TransactionStatus::Aborted => write!(f, "Aborted"),
118 }
119 }
120}
121
122#[derive(Debug, Clone)]
124pub struct Transaction {
125 pub id: TransactionId,
127 pub operations: Vec<TxOperation>,
129 pub status: TransactionStatus,
131 pub started_at: u64,
133 pub ended_at: Option<u64>,
135}
136
137impl Transaction {
138 fn new(id: TransactionId, started_at: u64) -> Self {
139 Self {
140 id,
141 operations: Vec::new(),
142 status: TransactionStatus::Active,
143 started_at,
144 ended_at: None,
145 }
146 }
147
148 #[inline]
150 pub fn is_active(&self) -> bool {
151 self.status == TransactionStatus::Active
152 }
153
154 #[inline]
156 pub fn operation_count(&self) -> usize {
157 self.operations.len()
158 }
159}
160
161#[derive(Debug, Clone, PartialEq)]
167pub struct TxStats {
168 pub active_count: usize,
170 pub committed_count: u64,
172 pub rolled_back_count: u64,
174 pub total_operations: u64,
176 pub avg_ops_per_tx: f64,
178}
179
180pub struct StorageTransactionLog {
193 transactions: VecDeque<Transaction>,
195 active: HashMap<TransactionId, Transaction>,
197 next_id: u64,
199 max_committed: usize,
201 committed_count: u64,
203 rolled_back_count: u64,
205}
206
207impl StorageTransactionLog {
208 pub fn new(max_committed: usize) -> Self {
218 Self {
219 transactions: VecDeque::new(),
220 active: HashMap::new(),
221 next_id: 1,
222 max_committed,
223 committed_count: 0,
224 rolled_back_count: 0,
225 }
226 }
227
228 pub fn begin(&mut self, now: u64) -> TransactionId {
236 let id = TransactionId(self.next_id);
237 self.next_id += 1;
238 let tx = Transaction::new(id, now);
239 self.active.insert(id, tx);
240 id
241 }
242
243 pub fn append(&mut self, tx_id: TransactionId, op: TxOperation) -> Result<(), TxError> {
250 let tx = self
251 .active
252 .get_mut(&tx_id)
253 .ok_or(TxError::TransactionNotFound(tx_id.0))?;
254 if !tx.is_active() {
255 return Err(TxError::TransactionNotActive(tx_id.0));
256 }
257 tx.operations.push(op);
258 Ok(())
259 }
260
261 pub fn commit(&mut self, tx_id: TransactionId, now: u64) -> Result<(), TxError> {
271 let mut tx = self
272 .active
273 .remove(&tx_id)
274 .ok_or(TxError::TransactionNotFound(tx_id.0))?;
275 if !tx.is_active() {
276 self.active.insert(tx_id, tx);
278 return Err(TxError::TransactionAlreadyEnded(tx_id.0));
279 }
280 tx.status = TransactionStatus::Committed;
281 tx.ended_at = Some(now);
282 self.committed_count += 1;
283
284 if self.max_committed > 0 && self.transactions.len() >= self.max_committed {
286 self.transactions.pop_front();
287 }
288 if self.max_committed > 0 {
289 self.transactions.push_back(tx);
290 }
291 Ok(())
293 }
294
295 pub fn rollback(
305 &mut self,
306 tx_id: TransactionId,
307 now: u64,
308 ) -> Result<Vec<TxOperation>, TxError> {
309 let mut tx = self
310 .active
311 .remove(&tx_id)
312 .ok_or(TxError::TransactionNotFound(tx_id.0))?;
313 if !tx.is_active() {
314 self.active.insert(tx_id, tx);
315 return Err(TxError::TransactionAlreadyEnded(tx_id.0));
316 }
317 tx.status = TransactionStatus::RolledBack;
318 tx.ended_at = Some(now);
319 self.rolled_back_count += 1;
320
321 let mut ops = tx.operations.clone();
323 ops.reverse();
324 Ok(ops)
327 }
328
329 pub fn abort(&mut self, tx_id: TransactionId, now: u64) -> Result<(), TxError> {
340 let mut tx = self
341 .active
342 .remove(&tx_id)
343 .ok_or(TxError::TransactionNotFound(tx_id.0))?;
344 if !tx.is_active() {
345 self.active.insert(tx_id, tx);
346 return Err(TxError::TransactionAlreadyEnded(tx_id.0));
347 }
348 tx.status = TransactionStatus::Aborted;
349 tx.ended_at = Some(now);
350 Ok(())
353 }
354
355 pub fn replay_committed(&self, since_id: TransactionId) -> Vec<&Transaction> {
365 self.transactions
366 .iter()
367 .filter(|tx| tx.id > since_id)
368 .collect()
369 }
370
371 pub fn get_transaction(&self, tx_id: TransactionId) -> Option<&Transaction> {
376 if let Some(tx) = self.active.get(&tx_id) {
377 return Some(tx);
378 }
379 self.transactions.iter().find(|tx| tx.id == tx_id)
380 }
381
382 pub fn active_transactions(&self) -> Vec<&Transaction> {
385 let mut txs: Vec<&Transaction> = self.active.values().collect();
386 txs.sort_by_key(|tx| tx.id);
387 txs
388 }
389
390 #[inline]
392 pub fn active_count(&self) -> usize {
393 self.active.len()
394 }
395
396 #[inline]
398 pub fn committed_count_total(&self) -> u64 {
399 self.committed_count
400 }
401
402 #[inline]
404 pub fn rolled_back_count_total(&self) -> u64 {
405 self.rolled_back_count
406 }
407
408 pub fn stats(&self) -> TxStats {
414 let total_operations: u64 = self
415 .transactions
416 .iter()
417 .map(|tx| tx.operations.len() as u64)
418 .sum();
419 let deque_len = self.transactions.len();
420 let avg_ops_per_tx = if deque_len == 0 {
421 0.0
422 } else {
423 total_operations as f64 / deque_len as f64
424 };
425 TxStats {
426 active_count: self.active.len(),
427 committed_count: self.committed_count,
428 rolled_back_count: self.rolled_back_count,
429 total_operations,
430 avg_ops_per_tx,
431 }
432 }
433
434 #[inline]
440 pub fn committed_retained(&self) -> usize {
441 self.transactions.len()
442 }
443
444 #[inline]
446 pub fn max_committed(&self) -> usize {
447 self.max_committed
448 }
449
450 #[inline]
452 pub fn next_transaction_id(&self) -> TransactionId {
453 TransactionId(self.next_id)
454 }
455
456 pub fn drain_committed(&mut self) -> Vec<Transaction> {
460 self.transactions.drain(..).collect()
461 }
462}
463
464#[cfg(test)]
469mod tests {
470 use crate::transaction_log::{
471 StorageTransactionLog, TransactionId, TransactionStatus, TxError, TxOperation, TxStats,
472 };
473
474 fn put_op(key: &str, value: &[u8]) -> TxOperation {
477 TxOperation::Put {
478 key: key.to_string(),
479 value: value.to_vec(),
480 prev_value: None,
481 }
482 }
483
484 fn put_op_with_prev(key: &str, value: &[u8], prev: &[u8]) -> TxOperation {
485 TxOperation::Put {
486 key: key.to_string(),
487 value: value.to_vec(),
488 prev_value: Some(prev.to_vec()),
489 }
490 }
491
492 fn delete_op(key: &str, prev: &[u8]) -> TxOperation {
493 TxOperation::Delete {
494 key: key.to_string(),
495 prev_value: prev.to_vec(),
496 }
497 }
498
499 fn batch_put_op(entries: &[(&str, &[u8])]) -> TxOperation {
500 TxOperation::BatchPut {
501 entries: entries
502 .iter()
503 .map(|(k, v)| (k.to_string(), v.to_vec()))
504 .collect(),
505 }
506 }
507
508 #[test]
511 fn test_new_log_is_empty() {
512 let log = StorageTransactionLog::new(100);
513 assert_eq!(log.active_count(), 0);
514 assert_eq!(log.committed_count_total(), 0);
515 assert_eq!(log.rolled_back_count_total(), 0);
516 assert_eq!(log.committed_retained(), 0);
517 }
518
519 #[test]
520 fn test_max_committed_preserved() {
521 let log = StorageTransactionLog::new(42);
522 assert_eq!(log.max_committed(), 42);
523 }
524
525 #[test]
526 fn test_next_id_starts_at_one() {
527 let log = StorageTransactionLog::new(10);
528 assert_eq!(log.next_transaction_id(), TransactionId(1));
529 }
530
531 #[test]
534 fn test_begin_returns_incrementing_ids() {
535 let mut log = StorageTransactionLog::new(10);
536 let id1 = log.begin(0);
537 let id2 = log.begin(1);
538 let id3 = log.begin(2);
539 assert_eq!(id1, TransactionId(1));
540 assert_eq!(id2, TransactionId(2));
541 assert_eq!(id3, TransactionId(3));
542 }
543
544 #[test]
545 fn test_begin_increments_active_count() {
546 let mut log = StorageTransactionLog::new(10);
547 log.begin(0);
548 assert_eq!(log.active_count(), 1);
549 log.begin(0);
550 assert_eq!(log.active_count(), 2);
551 }
552
553 #[test]
554 fn test_begin_transaction_is_active_status() {
555 let mut log = StorageTransactionLog::new(10);
556 let id = log.begin(100);
557 let tx = log.get_transaction(id).expect("tx must exist");
558 assert_eq!(tx.status, TransactionStatus::Active);
559 assert_eq!(tx.started_at, 100);
560 assert!(tx.ended_at.is_none());
561 }
562
563 #[test]
566 fn test_append_put_operation() {
567 let mut log = StorageTransactionLog::new(10);
568 let id = log.begin(0);
569 let result = log.append(id, put_op("k", b"v"));
570 assert!(result.is_ok());
571 let tx = log.get_transaction(id).expect("tx must exist");
572 assert_eq!(tx.operations.len(), 1);
573 }
574
575 #[test]
576 fn test_append_multiple_operations() {
577 let mut log = StorageTransactionLog::new(10);
578 let id = log.begin(0);
579 log.append(id, put_op("a", b"1")).unwrap();
580 log.append(id, put_op("b", b"2")).unwrap();
581 log.append(id, delete_op("c", b"old")).unwrap();
582 let tx = log.get_transaction(id).expect("tx must exist");
583 assert_eq!(tx.operations.len(), 3);
584 }
585
586 #[test]
587 fn test_append_unknown_tx_returns_not_found() {
588 let mut log = StorageTransactionLog::new(10);
589 let err = log
590 .append(TransactionId(99), put_op("k", b"v"))
591 .unwrap_err();
592 assert_eq!(err, TxError::TransactionNotFound(99));
593 }
594
595 #[test]
596 fn test_append_batch_put_operation() {
597 let mut log = StorageTransactionLog::new(10);
598 let id = log.begin(0);
599 let op = batch_put_op(&[("x", b"1"), ("y", b"2"), ("z", b"3")]);
600 log.append(id, op).unwrap();
601 let tx = log.get_transaction(id).unwrap();
602 assert_eq!(tx.operations.len(), 1);
603 if let TxOperation::BatchPut { entries } = &tx.operations[0] {
604 assert_eq!(entries.len(), 3);
605 } else {
606 panic!("expected BatchPut");
607 }
608 }
609
610 #[test]
613 fn test_commit_moves_tx_to_committed() {
614 let mut log = StorageTransactionLog::new(10);
615 let id = log.begin(0);
616 log.append(id, put_op("k", b"v")).unwrap();
617 log.commit(id, 1).unwrap();
618 assert_eq!(log.active_count(), 0);
619 assert_eq!(log.committed_count_total(), 1);
620 assert_eq!(log.committed_retained(), 1);
621 }
622
623 #[test]
624 fn test_commit_sets_ended_at() {
625 let mut log = StorageTransactionLog::new(10);
626 let id = log.begin(0);
627 log.commit(id, 42).unwrap();
628 let tx = log.get_transaction(id).expect("committed tx visible");
629 assert_eq!(tx.ended_at, Some(42));
630 assert_eq!(tx.status, TransactionStatus::Committed);
631 }
632
633 #[test]
634 fn test_commit_unknown_tx_returns_not_found() {
635 let mut log = StorageTransactionLog::new(10);
636 let err = log.commit(TransactionId(77), 0).unwrap_err();
637 assert_eq!(err, TxError::TransactionNotFound(77));
638 }
639
640 #[test]
641 fn test_commit_evicts_oldest_when_at_capacity() {
642 let mut log = StorageTransactionLog::new(3);
643 let ids: Vec<_> = (0..4).map(|t| log.begin(t)).collect();
644 for (i, &id) in ids.iter().enumerate() {
645 log.commit(id, i as u64 + 10).unwrap();
646 }
647 assert_eq!(log.committed_retained(), 3);
648 assert!(log.get_transaction(TransactionId(1)).is_none());
650 assert!(log.get_transaction(TransactionId(2)).is_some());
652 assert!(log.get_transaction(TransactionId(3)).is_some());
653 assert!(log.get_transaction(TransactionId(4)).is_some());
654 }
655
656 #[test]
657 fn test_commit_with_max_zero_retains_nothing() {
658 let mut log = StorageTransactionLog::new(0);
659 let id = log.begin(0);
660 log.commit(id, 1).unwrap();
661 assert_eq!(log.committed_retained(), 0);
662 assert_eq!(log.committed_count_total(), 1);
663 }
664
665 #[test]
668 fn test_rollback_returns_ops_in_reverse() {
669 let mut log = StorageTransactionLog::new(10);
670 let id = log.begin(0);
671 log.append(id, put_op("a", b"1")).unwrap();
672 log.append(id, put_op("b", b"2")).unwrap();
673 log.append(id, delete_op("c", b"old")).unwrap();
674 let ops = log.rollback(id, 5).unwrap();
675 assert_eq!(ops.len(), 3);
676 assert_eq!(ops[0], delete_op("c", b"old"));
678 assert_eq!(ops[1], put_op("b", b"2"));
679 assert_eq!(ops[2], put_op("a", b"1"));
680 }
681
682 #[test]
683 fn test_rollback_increments_rolled_back_count() {
684 let mut log = StorageTransactionLog::new(10);
685 let id = log.begin(0);
686 log.rollback(id, 1).unwrap();
687 assert_eq!(log.rolled_back_count_total(), 1);
688 }
689
690 #[test]
691 fn test_rollback_removes_from_active() {
692 let mut log = StorageTransactionLog::new(10);
693 let id = log.begin(0);
694 log.rollback(id, 1).unwrap();
695 assert_eq!(log.active_count(), 0);
696 }
697
698 #[test]
699 fn test_rollback_does_not_add_to_committed_deque() {
700 let mut log = StorageTransactionLog::new(10);
701 let id = log.begin(0);
702 log.rollback(id, 1).unwrap();
703 assert_eq!(log.committed_retained(), 0);
704 }
705
706 #[test]
707 fn test_rollback_unknown_tx_returns_not_found() {
708 let mut log = StorageTransactionLog::new(10);
709 let err = log.rollback(TransactionId(5), 0).unwrap_err();
710 assert_eq!(err, TxError::TransactionNotFound(5));
711 }
712
713 #[test]
714 fn test_rollback_empty_tx_returns_empty_ops() {
715 let mut log = StorageTransactionLog::new(10);
716 let id = log.begin(0);
717 let ops = log.rollback(id, 1).unwrap();
718 assert!(ops.is_empty());
719 }
720
721 #[test]
724 fn test_abort_removes_from_active() {
725 let mut log = StorageTransactionLog::new(10);
726 let id = log.begin(0);
727 log.abort(id, 1).unwrap();
728 assert_eq!(log.active_count(), 0);
729 }
730
731 #[test]
732 fn test_abort_does_not_increment_committed_count() {
733 let mut log = StorageTransactionLog::new(10);
734 let id = log.begin(0);
735 log.abort(id, 1).unwrap();
736 assert_eq!(log.committed_count_total(), 0);
737 }
738
739 #[test]
740 fn test_abort_does_not_increment_rolled_back_count() {
741 let mut log = StorageTransactionLog::new(10);
742 let id = log.begin(0);
743 log.abort(id, 1).unwrap();
744 assert_eq!(log.rolled_back_count_total(), 0);
745 }
746
747 #[test]
748 fn test_abort_unknown_tx_returns_not_found() {
749 let mut log = StorageTransactionLog::new(10);
750 let err = log.abort(TransactionId(9), 0).unwrap_err();
751 assert_eq!(err, TxError::TransactionNotFound(9));
752 }
753
754 #[test]
755 fn test_abort_does_not_retain_in_deque() {
756 let mut log = StorageTransactionLog::new(10);
757 let id = log.begin(0);
758 log.abort(id, 1).unwrap();
759 assert_eq!(log.committed_retained(), 0);
760 }
761
762 #[test]
765 fn test_replay_committed_since_zero_returns_all() {
766 let mut log = StorageTransactionLog::new(10);
767 let id1 = log.begin(0);
768 log.commit(id1, 1).unwrap();
769 let id2 = log.begin(2);
770 log.commit(id2, 3).unwrap();
771 let replayed = log.replay_committed(TransactionId(0));
772 assert_eq!(replayed.len(), 2);
773 }
774
775 #[test]
776 fn test_replay_committed_filters_by_id() {
777 let mut log = StorageTransactionLog::new(10);
778 let id1 = log.begin(0);
779 log.commit(id1, 1).unwrap();
780 let id2 = log.begin(2);
781 log.commit(id2, 3).unwrap();
782 let id3 = log.begin(4);
783 log.commit(id3, 5).unwrap();
784 let replayed = log.replay_committed(id1);
786 assert_eq!(replayed.len(), 2);
787 assert_eq!(replayed[0].id, id2);
788 assert_eq!(replayed[1].id, id3);
789 }
790
791 #[test]
792 fn test_replay_committed_returns_empty_for_high_since_id() {
793 let mut log = StorageTransactionLog::new(10);
794 let id = log.begin(0);
795 log.commit(id, 1).unwrap();
796 let replayed = log.replay_committed(TransactionId(999));
797 assert!(replayed.is_empty());
798 }
799
800 #[test]
801 fn test_replay_committed_order_is_ascending() {
802 let mut log = StorageTransactionLog::new(10);
803 for t in 0..5u64 {
804 let id = log.begin(t);
805 log.commit(id, t + 1).unwrap();
806 }
807 let replayed = log.replay_committed(TransactionId(0));
808 let ids: Vec<u64> = replayed.iter().map(|tx| tx.id.0).collect();
809 let mut sorted = ids.clone();
810 sorted.sort_unstable();
811 assert_eq!(ids, sorted);
812 }
813
814 #[test]
817 fn test_get_transaction_active() {
818 let mut log = StorageTransactionLog::new(10);
819 let id = log.begin(7);
820 let tx = log.get_transaction(id).expect("should find active tx");
821 assert_eq!(tx.id, id);
822 assert_eq!(tx.status, TransactionStatus::Active);
823 }
824
825 #[test]
826 fn test_get_transaction_committed() {
827 let mut log = StorageTransactionLog::new(10);
828 let id = log.begin(0);
829 log.commit(id, 1).unwrap();
830 let tx = log.get_transaction(id).expect("should find committed tx");
831 assert_eq!(tx.status, TransactionStatus::Committed);
832 }
833
834 #[test]
835 fn test_get_transaction_returns_none_for_unknown() {
836 let log = StorageTransactionLog::new(10);
837 assert!(log.get_transaction(TransactionId(42)).is_none());
838 }
839
840 #[test]
843 fn test_active_transactions_sorted_by_id() {
844 let mut log = StorageTransactionLog::new(10);
845 let _id3 = log.begin(2);
846 let _id1 = log.begin(0);
847 let _id2 = log.begin(1);
848 let active = log.active_transactions();
849 let ids: Vec<u64> = active.iter().map(|tx| tx.id.0).collect();
850 let mut sorted = ids.clone();
851 sorted.sort_unstable();
852 assert_eq!(ids, sorted);
853 }
854
855 #[test]
856 fn test_active_transactions_empty_after_all_committed() {
857 let mut log = StorageTransactionLog::new(10);
858 let ids: Vec<_> = (0..3).map(|t| log.begin(t)).collect();
859 for id in ids {
860 log.commit(id, 99).unwrap();
861 }
862 assert!(log.active_transactions().is_empty());
863 }
864
865 #[test]
868 fn test_stats_initial() {
869 let log = StorageTransactionLog::new(10);
870 let s = log.stats();
871 assert_eq!(
872 s,
873 TxStats {
874 active_count: 0,
875 committed_count: 0,
876 rolled_back_count: 0,
877 total_operations: 0,
878 avg_ops_per_tx: 0.0,
879 }
880 );
881 }
882
883 #[test]
884 fn test_stats_reflects_active_and_committed() {
885 let mut log = StorageTransactionLog::new(10);
886 let id1 = log.begin(0);
887 log.append(id1, put_op("a", b"1")).unwrap();
888 log.append(id1, put_op("b", b"2")).unwrap();
889 log.commit(id1, 1).unwrap();
890
891 let _id2 = log.begin(2);
892
893 let s = log.stats();
894 assert_eq!(s.active_count, 1);
895 assert_eq!(s.committed_count, 1);
896 assert_eq!(s.rolled_back_count, 0);
897 assert_eq!(s.total_operations, 2);
898 assert!((s.avg_ops_per_tx - 2.0).abs() < f64::EPSILON);
899 }
900
901 #[test]
902 fn test_stats_avg_ops_zero_when_no_committed_in_deque() {
903 let mut log = StorageTransactionLog::new(0); let id = log.begin(0);
905 log.append(id, put_op("k", b"v")).unwrap();
906 log.commit(id, 1).unwrap();
907 let s = log.stats();
908 assert_eq!(s.avg_ops_per_tx, 0.0);
909 assert_eq!(s.total_operations, 0);
910 }
911
912 #[test]
915 fn test_tx_operation_key_count_put() {
916 assert_eq!(put_op("k", b"v").key_count(), 1);
917 }
918
919 #[test]
920 fn test_tx_operation_key_count_delete() {
921 assert_eq!(delete_op("k", b"v").key_count(), 1);
922 }
923
924 #[test]
925 fn test_tx_operation_key_count_batch_put() {
926 let op = batch_put_op(&[("a", b"1"), ("b", b"2"), ("c", b"3"), ("d", b"4")]);
927 assert_eq!(op.key_count(), 4);
928 }
929
930 #[test]
933 fn test_drain_committed_returns_and_clears() {
934 let mut log = StorageTransactionLog::new(10);
935 let id1 = log.begin(0);
936 log.commit(id1, 1).unwrap();
937 let id2 = log.begin(2);
938 log.commit(id2, 3).unwrap();
939 let drained = log.drain_committed();
940 assert_eq!(drained.len(), 2);
941 assert_eq!(log.committed_retained(), 0);
942 }
943
944 #[test]
947 fn test_interleaved_transactions() {
948 let mut log = StorageTransactionLog::new(10);
949 let id_a = log.begin(0);
950 let id_b = log.begin(0);
951 log.append(id_a, put_op("a", b"va")).unwrap();
952 log.append(id_b, put_op("b", b"vb")).unwrap();
953 log.append(id_a, put_op("a2", b"va2")).unwrap();
954 log.commit(id_b, 1).unwrap();
955 log.rollback(id_a, 2).unwrap();
956 assert_eq!(log.committed_count_total(), 1);
957 assert_eq!(log.rolled_back_count_total(), 1);
958 assert_eq!(log.active_count(), 0);
959 }
960
961 #[test]
962 fn test_rollback_with_prev_values() {
963 let mut log = StorageTransactionLog::new(10);
964 let id = log.begin(0);
965 log.append(id, put_op_with_prev("k", b"new", b"old"))
966 .unwrap();
967 let ops = log.rollback(id, 1).unwrap();
968 assert_eq!(ops.len(), 1);
969 if let TxOperation::Put { prev_value, .. } = &ops[0] {
970 assert_eq!(prev_value.as_deref(), Some(b"old".as_slice()));
971 } else {
972 panic!("expected Put op");
973 }
974 }
975
976 #[test]
979 fn test_commit_does_not_affect_other_active_txs() {
980 let mut log = StorageTransactionLog::new(10);
981 let id_a = log.begin(0);
982 let id_b = log.begin(0);
983 log.commit(id_a, 1).unwrap();
984 assert_eq!(log.active_count(), 1);
985 assert!(log.get_transaction(id_b).is_some());
986 }
987
988 #[test]
989 fn test_large_batch_eviction_boundary() {
990 let mut log = StorageTransactionLog::new(5);
991 let ids: Vec<_> = (0..10u64).map(|t| log.begin(t)).collect();
992 for (i, &id) in ids.iter().enumerate() {
993 log.commit(id, i as u64 + 100).unwrap();
994 }
995 assert_eq!(log.committed_retained(), 5);
996 assert_eq!(log.committed_count_total(), 10);
997 let retained = log.replay_committed(TransactionId(0));
999 let min_id = retained.iter().map(|tx| tx.id.0).min().unwrap_or(0);
1000 assert_eq!(min_id, 6);
1001 }
1002
1003 #[test]
1004 fn test_transaction_id_ordering() {
1005 assert!(TransactionId(1) < TransactionId(2));
1006 assert!(TransactionId(100) > TransactionId(50));
1007 assert_eq!(TransactionId(5), TransactionId(5));
1008 }
1009
1010 #[test]
1011 fn test_transaction_id_display() {
1012 let id = TransactionId(7);
1013 assert_eq!(format!("{id}"), "Tx(7)");
1014 }
1015
1016 #[test]
1017 fn test_tx_error_display() {
1018 assert_eq!(
1019 format!("{}", TxError::TransactionNotFound(3)),
1020 "transaction 3 not found"
1021 );
1022 assert_eq!(
1023 format!("{}", TxError::TransactionNotActive(5)),
1024 "transaction 5 is not active"
1025 );
1026 assert_eq!(
1027 format!("{}", TxError::TransactionAlreadyEnded(9)),
1028 "transaction 9 has already ended"
1029 );
1030 }
1031
1032 #[test]
1033 fn test_many_operations_in_single_tx() {
1034 let mut log = StorageTransactionLog::new(10);
1035 let id = log.begin(0);
1036 for i in 0u64..500 {
1037 log.append(id, put_op(&format!("key-{i}"), b"value"))
1038 .unwrap();
1039 }
1040 let tx = log.get_transaction(id).unwrap();
1041 assert_eq!(tx.operation_count(), 500);
1042 log.commit(id, 1).unwrap();
1043 let s = log.stats();
1044 assert_eq!(s.total_operations, 500);
1045 }
1046}