Skip to main content

reddb_server/storage/engine/
overflow.rs

1//! Native compatibility paths for the shared overflow-chain implementation.
2pub 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            // one page
66            roundtrip(&pager, 1);
67            roundtrip(&pager, 100);
68            roundtrip(&pager, OVERFLOW_PAYLOAD_PER_PAGE - 1);
69            // exact one-page boundary
70            roundtrip(&pager, OVERFLOW_PAYLOAD_PER_PAGE);
71            // two pages
72            roundtrip(&pager, OVERFLOW_PAYLOAD_PER_PAGE + 1);
73            roundtrip(&pager, OVERFLOW_PAYLOAD_PER_PAGE * 2);
74            // several pages
75            roundtrip(&pager, OVERFLOW_PAYLOAD_PER_PAGE * 5 + 123);
76            // many pages
77            roundtrip(&pager, OVERFLOW_PAYLOAD_PER_PAGE * 32);
78            // exact multi-page boundary
79            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            // Too short: chain reports more bytes than caller expects.
119            let err = chain.read(head, total - 10).unwrap_err();
120            assert!(matches!(err, OverflowError::LengthMismatch { .. }));
121
122            // Too long: chain ends before caller's expected length.
123            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            // A second store of the same size must reuse the freed pages
151            // rather than extending the file.
152            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            // free does not shrink the file
178            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}