reddb_server/storage/engine/
overflow.rs1pub use reddb_engine::storage::overflow::{
3 pages_needed, OverflowError, OVERFLOW_PAGE_HEADER, OVERFLOW_PAYLOAD_PER_PAGE,
4};
5pub type OverflowChain<'p> =
6 reddb_engine::storage::overflow::OverflowChain<'p, super::pager::Pager>;
7
8#[cfg(test)]
9mod tests {
10 use super::*;
11 use crate::storage::engine::pager::Pager;
12 use std::path::PathBuf;
13 use std::sync::atomic::{AtomicU64, Ordering};
14
15 fn temp_db_path() -> PathBuf {
16 static COUNTER: AtomicU64 = AtomicU64::new(0);
17 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
18 let mut path = std::env::temp_dir();
19 path.push(format!(
20 "reddb_overflow_test_{}_{}.db",
21 std::process::id(),
22 id
23 ));
24 path
25 }
26
27 fn cleanup(path: &std::path::Path) {
28 let _ = std::fs::remove_file(path);
29 for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
30 let _ = std::fs::remove_file(sidecar);
31 }
32 }
33
34 fn pattern(len: usize) -> Vec<u8> {
35 (0..len).map(|i| ((i * 31 + 7) & 0xff) as u8).collect()
36 }
37
38 #[test]
39 fn pages_needed_boundaries() {
40 assert_eq!(pages_needed(0), 1);
41 assert_eq!(pages_needed(1), 1);
42 assert_eq!(pages_needed(OVERFLOW_PAYLOAD_PER_PAGE), 1);
43 assert_eq!(pages_needed(OVERFLOW_PAYLOAD_PER_PAGE + 1), 2);
44 assert_eq!(pages_needed(OVERFLOW_PAYLOAD_PER_PAGE * 4), 4);
45 assert_eq!(pages_needed(OVERFLOW_PAYLOAD_PER_PAGE * 4 + 1), 5);
46 }
47
48 fn roundtrip(pager: &Pager, len: usize) {
49 let chain = OverflowChain::new(pager);
50 let data = pattern(len);
51 let (head, total) = chain.store(&data).unwrap();
52 assert_eq!(total, len as u64);
53 let read_back = chain.read(head, total).unwrap();
54 assert_eq!(read_back.len(), len);
55 assert_eq!(read_back, data);
56 chain.free(head).unwrap();
57 }
58
59 #[test]
60 fn store_read_roundtrips_across_sizes() {
61 let path = temp_db_path();
62 cleanup(&path);
63 {
64 let pager = Pager::open_default(&path).unwrap();
65 roundtrip(&pager, 1);
67 roundtrip(&pager, 100);
68 roundtrip(&pager, OVERFLOW_PAYLOAD_PER_PAGE - 1);
69 roundtrip(&pager, OVERFLOW_PAYLOAD_PER_PAGE);
71 roundtrip(&pager, OVERFLOW_PAYLOAD_PER_PAGE + 1);
73 roundtrip(&pager, OVERFLOW_PAYLOAD_PER_PAGE * 2);
74 roundtrip(&pager, OVERFLOW_PAYLOAD_PER_PAGE * 5 + 123);
76 roundtrip(&pager, OVERFLOW_PAYLOAD_PER_PAGE * 32);
78 roundtrip(&pager, OVERFLOW_PAYLOAD_PER_PAGE * 7);
80 }
81 cleanup(&path);
82 }
83
84 #[test]
85 fn store_empty_value_produces_single_page() {
86 let path = temp_db_path();
87 cleanup(&path);
88 {
89 let pager = Pager::open_default(&path).unwrap();
90 let chain = OverflowChain::new(&pager);
91 let pages_before = pager.page_count().unwrap();
92 let (head, total) = chain.store(&[]).unwrap();
93 assert_eq!(total, 0);
94 let bytes = chain.read(head, total).unwrap();
95 assert!(bytes.is_empty());
96
97 assert_eq!(
98 pager.page_count().unwrap(),
99 pages_before + 1,
100 "empty value allocates exactly one terminating head page"
101 );
102
103 chain.free(head).unwrap();
104 }
105 cleanup(&path);
106 }
107
108 #[test]
109 fn read_with_wrong_total_len_errors() {
110 let path = temp_db_path();
111 cleanup(&path);
112 {
113 let pager = Pager::open_default(&path).unwrap();
114 let chain = OverflowChain::new(&pager);
115 let data = pattern(OVERFLOW_PAYLOAD_PER_PAGE * 3 + 50);
116 let (head, total) = chain.store(&data).unwrap();
117
118 let err = chain.read(head, total - 10).unwrap_err();
120 assert!(matches!(err, OverflowError::LengthMismatch { .. }));
121
122 let err = chain.read(head, total + 10).unwrap_err();
124 assert!(matches!(err, OverflowError::LengthMismatch { .. }));
125
126 chain.free(head).unwrap();
127 }
128 cleanup(&path);
129 }
130
131 #[test]
132 fn free_returns_pages_to_freelist_observably() {
133 let path = temp_db_path();
134 cleanup(&path);
135 {
136 let pager = Pager::open_default(&path).unwrap();
137 let chain = OverflowChain::new(&pager);
138
139 let len = OVERFLOW_PAYLOAD_PER_PAGE * 6 + 17;
140 let n = pages_needed(len);
141 let data = pattern(len);
142
143 let before_alloc = pager.page_count().unwrap();
144 let (head, _) = chain.store(&data).unwrap();
145 let after_alloc = pager.page_count().unwrap();
146 assert_eq!((after_alloc - before_alloc) as usize, n);
147
148 chain.free(head).unwrap();
149
150 let after_free = pager.page_count().unwrap();
153 let (head2, _) = chain.store(&data).unwrap();
154 let after_realloc = pager.page_count().unwrap();
155 assert_eq!(
156 after_realloc, after_free,
157 "second store should reuse freed pages"
158 );
159
160 chain.free(head2).unwrap();
161 }
162 cleanup(&path);
163 }
164
165 #[test]
166 fn free_then_store_reuses_pages_exact_count() {
167 let path = temp_db_path();
168 cleanup(&path);
169 {
170 let pager = Pager::open_default(&path).unwrap();
171 let chain = OverflowChain::new(&pager);
172
173 let len = OVERFLOW_PAYLOAD_PER_PAGE * 4;
174 let (head, _) = chain.store(&pattern(len)).unwrap();
175 let baseline = pager.page_count().unwrap();
176 chain.free(head).unwrap();
177 assert_eq!(pager.page_count().unwrap(), baseline);
179
180 let (head2, _) = chain.store(&pattern(len)).unwrap();
181 assert_eq!(pager.page_count().unwrap(), baseline);
182 chain.free(head2).unwrap();
183 }
184 cleanup(&path);
185 }
186}