1use std::collections::HashMap;
25use std::io;
26use std::path::{Path, PathBuf};
27use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
28
29use parking_lot::{Mutex, MutexGuard};
30
31use super::append_coordinator::WalAppendCoordinator;
32use super::record::WalRecord;
33use super::writer::WalWriter;
34use crate::storage::engine::{Page, Pager, PAGE_SIZE};
35use crate::storage::transaction::snapshot::SnapshotManager;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum TxState {
40 Active,
42 Committed,
44 Aborted,
46}
47
48#[derive(Debug)]
50pub enum TxError {
51 Io(io::Error),
53 Pager(String),
55 LockPoisoned(&'static str),
57 NotActive,
59 AlreadyCommitted,
61 AlreadyAborted,
63 WriteConflict(u32),
65 InvalidPage(String),
67}
68
69impl std::fmt::Display for TxError {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 match self {
72 Self::Io(e) => write!(f, "I/O error: {}", e),
73 Self::Pager(msg) => write!(f, "Pager error: {}", msg),
74 Self::LockPoisoned(name) => write!(f, "Lock poisoned: {}", name),
75 Self::NotActive => write!(f, "Transaction is not active"),
76 Self::AlreadyCommitted => write!(f, "Transaction already committed"),
77 Self::AlreadyAborted => write!(f, "Transaction already aborted"),
78 Self::WriteConflict(page_id) => write!(f, "Write conflict on page {}", page_id),
79 Self::InvalidPage(msg) => write!(f, "Invalid page: {}", msg),
80 }
81 }
82}
83
84impl std::error::Error for TxError {}
85
86impl From<io::Error> for TxError {
87 fn from(e: io::Error) -> Self {
88 Self::Io(e)
89 }
90}
91
92#[derive(Clone)]
94struct BufferedWrite {
95 page_id: u32,
96 data: [u8; PAGE_SIZE],
97}
98
99pub struct Transaction {
103 id: u64,
105 state: TxState,
107 write_set: HashMap<u32, BufferedWrite>,
109 read_set: Vec<u32>,
111 manager: Arc<TransactionManager>,
113}
114
115impl Transaction {
116 pub fn id(&self) -> u64 {
118 self.id
119 }
120
121 pub fn state(&self) -> TxState {
123 self.state
124 }
125
126 pub fn is_active(&self) -> bool {
128 self.state == TxState::Active
129 }
130
131 pub fn read_page(&mut self, page_id: u32) -> Result<Page, TxError> {
136 if self.state != TxState::Active {
137 return Err(TxError::NotActive);
138 }
139
140 if let Some(buffered) = self.write_set.get(&page_id) {
142 return Ok(Page::from_bytes(buffered.data));
143 }
144
145 self.read_set.push(page_id);
147
148 self.manager
150 .pager
151 .read_page(page_id)
152 .map_err(|e| TxError::Pager(e.to_string()))
153 }
154
155 pub fn write_page(&mut self, page_id: u32, page: Page) -> Result<(), TxError> {
159 if self.state != TxState::Active {
160 return Err(TxError::NotActive);
161 }
162
163 let mut data = [0u8; PAGE_SIZE];
165 data.copy_from_slice(page.as_bytes());
166
167 self.write_set
168 .insert(page_id, BufferedWrite { page_id, data });
169
170 Ok(())
171 }
172
173 pub fn commit(mut self) -> Result<(), TxError> {
186 if self.state != TxState::Active {
187 return match self.state {
188 TxState::Committed => Err(TxError::AlreadyCommitted),
189 TxState::Aborted => Err(TxError::AlreadyAborted),
190 _ => Err(TxError::NotActive),
191 };
192 }
193
194 if self.write_set.is_empty() {
199 self.state = TxState::Committed;
200 self.manager.unregister_transaction(self.id);
201 self.manager.snapshot_manager.commit(self.id);
202 return Ok(());
203 }
204
205 let mut blob = Vec::with_capacity(64 + self.write_set.len() * (PAGE_SIZE + 32));
213 for (page_id, buffered) in &self.write_set {
214 let record = WalRecord::PageWrite {
215 tx_id: self.id,
216 page_id: *page_id,
217 data: buffered.data.to_vec(),
218 };
219 record.encode_into(&mut blob);
223 }
224 WalRecord::Commit { tx_id: self.id }.encode_into(&mut blob);
225
226 let commit_lsn = self.manager.coordinator.reserve_and_enqueue(blob);
231
232 self.manager
239 .coordinator
240 .commit_at_least(commit_lsn, &self.manager.wal)
241 .map_err(TxError::Io)?;
242
243 for (page_id, buffered) in &self.write_set {
245 let page = Page::from_bytes(buffered.data);
246 self.manager
247 .pager
248 .write_page(*page_id, page)
249 .map_err(|e| TxError::Pager(e.to_string()))?;
250 }
251
252 self.state = TxState::Committed;
253
254 self.manager.unregister_transaction(self.id);
256 self.manager.snapshot_manager.commit(self.id);
257
258 Ok(())
259 }
260
261 pub fn rollback(mut self) -> Result<(), TxError> {
265 if self.state != TxState::Active {
266 return match self.state {
267 TxState::Committed => Err(TxError::AlreadyCommitted),
268 TxState::Aborted => Err(TxError::AlreadyAborted),
269 _ => Err(TxError::NotActive),
270 };
271 }
272
273 let blob = WalRecord::Rollback { tx_id: self.id }.encode();
278 let target = self.manager.coordinator.reserve_and_enqueue(blob);
279 self.manager
280 .coordinator
281 .commit_at_least(target, &self.manager.wal)
282 .map_err(TxError::Io)?;
283
284 self.write_set.clear();
286 self.state = TxState::Aborted;
287
288 self.manager.unregister_transaction(self.id);
290 self.manager.snapshot_manager.rollback(self.id);
291
292 Ok(())
293 }
294}
295
296impl Drop for Transaction {
297 fn drop(&mut self) {
298 if self.state == TxState::Active {
301 let blob = WalRecord::Rollback { tx_id: self.id }.encode();
307 let target = self.manager.coordinator.reserve_and_enqueue(blob);
308 let _ = self
309 .manager
310 .coordinator
311 .commit_at_least(target, &self.manager.wal);
312 self.manager.unregister_transaction(self.id);
313 self.manager.snapshot_manager.rollback(self.id);
314 }
315 }
316}
317
318pub struct TransactionManager {
322 pager: Arc<Pager>,
324 wal: Mutex<WalWriter>,
329 wal_path: PathBuf,
331 active_transactions: RwLock<Vec<u64>>,
333 snapshot_manager: Arc<SnapshotManager>,
335 coordinator: WalAppendCoordinator,
344}
345
346impl TransactionManager {
347 pub fn new(pager: Arc<Pager>, wal_path: impl AsRef<Path>) -> io::Result<Self> {
354 Self::new_with_snapshot_manager(pager, wal_path, Arc::new(SnapshotManager::new()))
355 }
356
357 pub fn new_with_snapshot_manager(
359 pager: Arc<Pager>,
360 wal_path: impl AsRef<Path>,
361 snapshot_manager: Arc<SnapshotManager>,
362 ) -> io::Result<Self> {
363 let wal_path = wal_path.as_ref().to_path_buf();
364 let wal = WalWriter::open(&wal_path)?;
365 let initial_current = wal.current_lsn();
366 let initial_durable = wal.durable_lsn();
367
368 Ok(Self {
369 pager,
370 wal: Mutex::new(wal),
371 wal_path,
372 active_transactions: RwLock::new(Vec::new()),
373 snapshot_manager,
374 coordinator: WalAppendCoordinator::new(initial_current, initial_durable),
375 })
376 }
377
378 fn wal_writer(&self) -> Result<MutexGuard<'_, WalWriter>, TxError> {
379 Ok(self.wal.lock())
386 }
387
388 fn active_transactions_write(&self) -> RwLockWriteGuard<'_, Vec<u64>> {
389 self.active_transactions
390 .write()
391 .unwrap_or_else(|poisoned| poisoned.into_inner())
392 }
393
394 fn active_transactions_read(&self) -> RwLockReadGuard<'_, Vec<u64>> {
395 self.active_transactions
396 .read()
397 .unwrap_or_else(|poisoned| poisoned.into_inner())
398 }
399
400 pub fn begin(self: &Arc<Self>) -> Result<Transaction, TxError> {
402 let tx_id = self.snapshot_manager.begin();
403
404 let blob = WalRecord::Begin { tx_id }.encode();
412 let _begin_lsn = self.coordinator.reserve_and_enqueue(blob);
413
414 {
416 let mut active = self.active_transactions_write();
417 active.push(tx_id);
418 }
419
420 Ok(Transaction {
421 id: tx_id,
422 state: TxState::Active,
423 write_set: HashMap::new(),
424 read_set: Vec::new(),
425 manager: Arc::clone(self),
426 })
427 }
428
429 fn unregister_transaction(&self, tx_id: u64) {
431 let mut active = self.active_transactions_write();
432 active.retain(|&id| id != tx_id);
433 }
434
435 pub fn active_transactions(&self) -> Vec<u64> {
437 self.active_transactions_read().clone()
438 }
439
440 pub fn wal_path(&self) -> &Path {
442 &self.wal_path
443 }
444
445 pub fn pager(&self) -> &Arc<Pager> {
447 &self.pager
448 }
449
450 pub fn sync_wal(&self) -> io::Result<()> {
454 let target = self.coordinator.next_lsn();
455 self.coordinator.commit_at_least(target, &self.wal)
456 }
457
458 pub fn has_active_transactions(&self) -> bool {
460 !self.active_transactions_read().is_empty()
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use crate::storage::engine::PageType;
468 use std::fs;
469 use std::time::{SystemTime, UNIX_EPOCH};
470
471 fn temp_dir() -> PathBuf {
472 let timestamp = SystemTime::now()
473 .duration_since(UNIX_EPOCH)
474 .expect("value is present")
475 .as_nanos();
476 std::env::temp_dir().join(format!("reddb_tx_test_{}", timestamp))
477 }
478
479 fn cleanup(dir: &Path) {
480 let _ = fs::remove_dir_all(dir);
481 }
482
483 fn temp_wal_path(dir: &Path, name: &str) -> PathBuf {
484 reddb_file::layout::wal_component_temp_path(dir, "transaction", name, std::process::id())
485 }
486
487 #[test]
488 fn test_transaction_commit() {
489 let dir = temp_dir();
490 let _ = fs::create_dir_all(&dir);
491 let db_path = dir.join("test.db");
492 let wal_path = temp_wal_path(&dir, "commit");
493
494 let pager = Arc::new(Pager::open_default(&db_path).expect("open_default() should succeed"));
496
497 let page = pager
499 .allocate_page(PageType::BTreeLeaf)
500 .expect("allocate_page() should succeed");
501 let page_id = page.page_id();
502
503 let tm = Arc::new(
505 TransactionManager::new(Arc::clone(&pager), &wal_path).expect("new() should succeed"),
506 );
507
508 let mut tx = tm.begin().expect("begin() should succeed");
510 assert!(tx.is_active());
511
512 let mut page = Page::new(PageType::BTreeLeaf, page_id);
514 page.as_bytes_mut()[100] = 0xAB;
515 tx.write_page(page_id, page)
516 .expect("write_page() should succeed");
517
518 let read_page = tx.read_page(page_id).expect("read_page() should succeed");
520 assert_eq!(read_page.as_bytes()[100], 0xAB);
521
522 tx.commit().expect("commit() should succeed");
524
525 let final_page = pager
527 .read_page(page_id)
528 .expect("read_page() should succeed");
529 assert_eq!(final_page.as_bytes()[100], 0xAB);
530
531 cleanup(&dir);
532 }
533
534 #[test]
535 fn test_transaction_rollback() {
536 let dir = temp_dir();
537 let _ = fs::create_dir_all(&dir);
538 let db_path = dir.join("test.db");
539 let wal_path = temp_wal_path(&dir, "rollback");
540
541 let pager = Arc::new(Pager::open_default(&db_path).expect("open_default() should succeed"));
543
544 let mut page = pager
546 .allocate_page(PageType::BTreeLeaf)
547 .expect("allocate_page() should succeed");
548 let page_id = page.page_id();
549 page.as_bytes_mut()[100] = 0x11;
550 pager
551 .write_page(page_id, page)
552 .expect("write_page() should succeed");
553
554 let tm = Arc::new(
556 TransactionManager::new(Arc::clone(&pager), &wal_path).expect("new() should succeed"),
557 );
558
559 let mut tx = tm.begin().expect("begin() should succeed");
561
562 let mut page = Page::new(PageType::BTreeLeaf, page_id);
564 page.as_bytes_mut()[100] = 0xAB;
565 tx.write_page(page_id, page)
566 .expect("write_page() should succeed");
567
568 let read_page = tx.read_page(page_id).expect("read_page() should succeed");
570 assert_eq!(read_page.as_bytes()[100], 0xAB);
571
572 tx.rollback().expect("rollback() should succeed");
574
575 let final_page = pager
577 .read_page(page_id)
578 .expect("read_page() should succeed");
579 assert_eq!(final_page.as_bytes()[100], 0x11);
580
581 cleanup(&dir);
582 }
583
584 #[test]
585 fn test_multiple_transactions() {
586 let dir = temp_dir();
587 let _ = fs::create_dir_all(&dir);
588 let db_path = dir.join("test.db");
589 let wal_path = temp_wal_path(&dir, "multiple");
590
591 let pager = Arc::new(Pager::open_default(&db_path).expect("open_default() should succeed"));
593
594 let page1 = pager
596 .allocate_page(PageType::BTreeLeaf)
597 .expect("allocate_page() should succeed");
598 let page2 = pager
599 .allocate_page(PageType::BTreeLeaf)
600 .expect("allocate_page() should succeed");
601 let page1_id = page1.page_id();
602 let page2_id = page2.page_id();
603
604 let tm = Arc::new(
606 TransactionManager::new(Arc::clone(&pager), &wal_path).expect("new() should succeed"),
607 );
608
609 let mut tx1 = tm.begin().expect("begin() should succeed");
611 let mut page1 = Page::new(PageType::BTreeLeaf, page1_id);
612 page1.as_bytes_mut()[100] = 0x11;
613 tx1.write_page(page1_id, page1)
614 .expect("write_page() should succeed");
615 tx1.commit().expect("commit() should succeed");
616
617 let mut tx2 = tm.begin().expect("begin() should succeed");
619 let mut page2 = Page::new(PageType::BTreeLeaf, page2_id);
620 page2.as_bytes_mut()[100] = 0x22;
621 tx2.write_page(page2_id, page2)
622 .expect("write_page() should succeed");
623 tx2.commit().expect("commit() should succeed");
624
625 let final_page1 = pager
627 .read_page(page1_id)
628 .expect("read_page() should succeed");
629 let final_page2 = pager
630 .read_page(page2_id)
631 .expect("read_page() should succeed");
632 assert_eq!(final_page1.as_bytes()[100], 0x11);
633 assert_eq!(final_page2.as_bytes()[100], 0x22);
634
635 cleanup(&dir);
636 }
637
638 #[test]
639 fn test_transaction_isolation() {
640 let dir = temp_dir();
641 let _ = fs::create_dir_all(&dir);
642 let db_path = dir.join("test.db");
643 let wal_path = temp_wal_path(&dir, "isolation");
644
645 let pager = Arc::new(Pager::open_default(&db_path).expect("open_default() should succeed"));
647
648 let mut page = pager
650 .allocate_page(PageType::BTreeLeaf)
651 .expect("allocate_page() should succeed");
652 let page_id = page.page_id();
653 page.as_bytes_mut()[100] = 0x00;
654 pager
655 .write_page(page_id, page)
656 .expect("write_page() should succeed");
657
658 let tm = Arc::new(
660 TransactionManager::new(Arc::clone(&pager), &wal_path).expect("new() should succeed"),
661 );
662
663 let mut tx1 = tm.begin().expect("begin() should succeed");
665 let mut page1 = Page::new(PageType::BTreeLeaf, page_id);
666 page1.as_bytes_mut()[100] = 0x11;
667 tx1.write_page(page_id, page1)
668 .expect("write_page() should succeed");
669
670 let tx1_read = tx1.read_page(page_id).expect("read_page() should succeed");
672 assert_eq!(tx1_read.as_bytes()[100], 0x11);
673
674 let pager_read = pager
676 .read_page(page_id)
677 .expect("read_page() should succeed");
678 assert_eq!(pager_read.as_bytes()[100], 0x00);
679
680 tx1.commit().expect("commit() should succeed");
682
683 let final_read = pager
685 .read_page(page_id)
686 .expect("read_page() should succeed");
687 assert_eq!(final_read.as_bytes()[100], 0x11);
688
689 cleanup(&dir);
690 }
691
692 #[test]
693 fn test_active_transaction_tracking() {
694 let dir = temp_dir();
695 let _ = fs::create_dir_all(&dir);
696 let db_path = dir.join("test.db");
697 let wal_path = temp_wal_path(&dir, "tracking");
698
699 let pager = Arc::new(Pager::open_default(&db_path).expect("open_default() should succeed"));
700 let tm = Arc::new(
701 TransactionManager::new(Arc::clone(&pager), &wal_path).expect("new() should succeed"),
702 );
703
704 assert!(!tm.has_active_transactions());
705
706 let tx1 = tm.begin().expect("begin() should succeed");
707 let tx1_id = tx1.id();
708 assert!(tm.has_active_transactions());
709 assert!(tm.active_transactions().contains(&tx1_id));
710
711 let tx2 = tm.begin().expect("begin() should succeed");
712 let tx2_id = tx2.id();
713 assert_eq!(tm.active_transactions().len(), 2);
714
715 tx1.commit().expect("commit() should succeed");
716 assert!(!tm.active_transactions().contains(&tx1_id));
717 assert!(tm.active_transactions().contains(&tx2_id));
718
719 tx2.rollback().expect("rollback() should succeed");
720 assert!(!tm.has_active_transactions());
721
722 cleanup(&dir);
723 }
724
725 #[test]
726 fn page_wal_transactions_allocate_from_snapshot_manager() {
727 let dir = temp_dir();
728 let _ = fs::create_dir_all(&dir);
729 let db_path = dir.join("test.db");
730 let wal_path = temp_wal_path(&dir, "snapshot-xid");
731
732 let pager = Arc::new(Pager::open_default(&db_path).expect("open_default() should succeed"));
733 let snapshot_manager =
734 Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
735 let row_xid = snapshot_manager.begin();
736 snapshot_manager.commit(row_xid);
737
738 let tm = Arc::new(
739 TransactionManager::new_with_snapshot_manager(
740 Arc::clone(&pager),
741 &wal_path,
742 Arc::clone(&snapshot_manager),
743 )
744 .expect("new() should succeed"),
745 );
746
747 let tx = tm.begin().expect("begin() should succeed");
748 assert_eq!(tx.id(), row_xid + 1);
749 tx.commit().expect("commit() should succeed");
750 assert_eq!(snapshot_manager.peek_next_xid(), row_xid + 2);
751
752 cleanup(&dir);
753 }
754
755 #[test]
756 fn test_transaction_double_commit() {
757 let dir = temp_dir();
758 let _ = fs::create_dir_all(&dir);
759 let db_path = dir.join("test.db");
760 let wal_path = temp_wal_path(&dir, "double-commit");
761
762 let pager = Arc::new(Pager::open_default(&db_path).expect("open_default() should succeed"));
763 let tm = Arc::new(
764 TransactionManager::new(Arc::clone(&pager), &wal_path).expect("new() should succeed"),
765 );
766
767 let tx = tm.begin().expect("begin() should succeed");
770 tx.commit().expect("commit() should succeed");
771
772 cleanup(&dir);
773 }
774
775 #[test]
776 fn test_begin_succeeds_after_panic_in_lock_holder() {
777 let dir = temp_dir();
784 let _ = fs::create_dir_all(&dir);
785 let db_path = dir.join("test.db");
786 let wal_path = temp_wal_path(&dir, "panic-lock-holder");
787
788 let pager = Arc::new(Pager::open_default(&db_path).expect("open_default() should succeed"));
789 let tm = Arc::new(
790 TransactionManager::new(Arc::clone(&pager), &wal_path).expect("new() should succeed"),
791 );
792
793 let poison_target = Arc::clone(&tm);
794 let _ = std::thread::spawn(move || {
795 let _guard = poison_target.wal.lock();
796 panic!("would-poison the wal mutex on std::sync");
797 })
798 .join();
799
800 match tm.begin() {
802 Ok(_) => {}
803 Err(err) => panic!("begin must succeed despite prior panic: {err:?}"),
804 }
805
806 cleanup(&dir);
807 }
808
809 #[test]
814 fn read_only_commit_does_not_advance_durable_lsn() {
815 let dir = temp_dir();
816 let _ = fs::create_dir_all(&dir);
817 let db_path = dir.join("ro_durable.db");
818 let wal_path = temp_wal_path(&dir, "ro-durable");
819
820 let pager = Arc::new(Pager::open_default(&db_path).expect("open_default() should succeed"));
821 let tm = Arc::new(
822 TransactionManager::new(Arc::clone(&pager), &wal_path).expect("new() should succeed"),
823 );
824
825 let before = {
827 let wal = tm.wal_writer().expect("wal_writer() should succeed");
828 wal.durable_lsn()
829 };
830
831 let tx = tm.begin().expect("begin() should succeed");
832 tx.commit().expect("commit() should succeed");
834
835 let after = {
838 let wal = tm.wal_writer().expect("wal_writer() should succeed");
839 wal.durable_lsn()
840 };
841 assert_eq!(
842 before, after,
843 "read-only commit must not advance durable_lsn (was {} → {})",
844 before, after
845 );
846
847 cleanup(&dir);
848 }
849
850 #[test]
851 fn read_only_commit_does_not_grow_wal_file() {
852 let dir = temp_dir();
853 let _ = fs::create_dir_all(&dir);
854 let db_path = dir.join("ro_size.db");
855 let wal_path = temp_wal_path(&dir, "ro-size");
856
857 let pager = Arc::new(Pager::open_default(&db_path).expect("open_default() should succeed"));
858 let tm = Arc::new(
859 TransactionManager::new(Arc::clone(&pager), &wal_path).expect("new() should succeed"),
860 );
861
862 let size_before = std::fs::metadata(&wal_path)
864 .expect("metadata() should succeed")
865 .len();
866 assert_eq!(
867 size_before,
868 reddb_file::WAL_FILE_HEADER_BYTES as u64,
869 "fresh WAL must be exactly the 8-byte header"
870 );
871
872 for _ in 0..100 {
874 let tx = tm.begin().expect("begin() should succeed");
875 tx.commit().expect("commit() should succeed");
876 }
877
878 let size_after = std::fs::metadata(&wal_path)
879 .expect("metadata() should succeed")
880 .len();
881 assert_eq!(
882 size_after, size_before,
883 "100 read-only commits should not have written any WAL bytes"
884 );
885 cleanup(&dir);
886 }
887
888 #[test]
889 fn read_only_commit_marks_transaction_committed() {
890 let dir = temp_dir();
891 let _ = fs::create_dir_all(&dir);
892 let db_path = dir.join("ro_state.db");
893 let wal_path = temp_wal_path(&dir, "ro-state");
894
895 let pager = Arc::new(Pager::open_default(&db_path).expect("open_default() should succeed"));
896 let tm = Arc::new(
897 TransactionManager::new(Arc::clone(&pager), &wal_path).expect("new() should succeed"),
898 );
899
900 let tx = tm.begin().expect("begin() should succeed");
901 let id = tx.id();
902 tx.commit().expect("commit() should succeed");
903
904 assert!(
907 !tm.active_transactions().contains(&id),
908 "RO-committed txn {id} must no longer be active in the manager"
909 );
910
911 cleanup(&dir);
912 }
913
914 #[test]
915 fn writing_commit_still_syncs_after_ro_fast_path() {
916 let dir = temp_dir();
920 let _ = fs::create_dir_all(&dir);
921 let db_path = dir.join("rw_after_ro.db");
922 let wal_path = temp_wal_path(&dir, "rw-after-ro");
923
924 let pager = Arc::new(Pager::open_default(&db_path).expect("open_default() should succeed"));
925 let allocated = pager
926 .allocate_page(PageType::BTreeLeaf)
927 .expect("allocate_page() should succeed");
928 let page_id = allocated.page_id();
929 let tm = Arc::new(
930 TransactionManager::new(Arc::clone(&pager), &wal_path).expect("new() should succeed"),
931 );
932
933 let ro = tm.begin().expect("begin() should succeed");
935 ro.commit().expect("commit() should succeed");
936
937 let mut rw = tm.begin().expect("begin() should succeed");
939 let mut page = Page::new(PageType::BTreeLeaf, page_id);
940 page.as_bytes_mut()[42] = 0x77;
941 rw.write_page(page_id, page)
942 .expect("write_page() should succeed");
943 rw.commit().expect("commit() should succeed");
944
945 let size = std::fs::metadata(&wal_path)
948 .expect("metadata() should succeed")
949 .len();
950 assert!(size > 8, "writing commit should grow the WAL");
951
952 let read_back = pager
954 .read_page(page_id)
955 .expect("read_page() should succeed");
956 assert_eq!(read_back.as_bytes()[42], 0x77);
957
958 cleanup(&dir);
959 }
960}