Skip to main content

reddb_server/storage/engine/btree/
value_layout.rs

1//! Native compatibility paths for the shared large-value layout.
2use crate::storage::engine::pager::Pager;
3pub use reddb_engine::storage::btree::value_layout::{
4    pointer_head, projected_cell_len, ValueLayoutError, MAX_VALUE_SIZE, OVERFLOW_THRESHOLD,
5    POINTER_CELL_LEN,
6};
7pub fn encode(pager: &Pager, value: &[u8]) -> Result<Vec<u8>, ValueLayoutError> {
8    reddb_engine::storage::btree::value_layout::encode(pager, value)
9}
10pub fn decode(pager: &Pager, stored: &[u8]) -> Result<Vec<u8>, ValueLayoutError> {
11    reddb_engine::storage::btree::value_layout::decode(pager, stored)
12}
13
14#[cfg(test)]
15mod tests {
16    use super::*;
17    use crate::storage::engine::pager::Pager;
18    use std::path::PathBuf;
19    use std::sync::atomic::{AtomicU64, Ordering};
20
21    fn temp_db_path() -> PathBuf {
22        static COUNTER: AtomicU64 = AtomicU64::new(0);
23        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
24        let mut path = std::env::temp_dir();
25        path.push(format!(
26            "reddb_value_layout_test_{}_{}.db",
27            std::process::id(),
28            id
29        ));
30        path
31    }
32
33    fn cleanup(path: &std::path::Path) {
34        let _ = std::fs::remove_file(path);
35        for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
36            let _ = std::fs::remove_file(sidecar);
37        }
38    }
39
40    fn fresh_pager() -> (Pager, PathBuf) {
41        let path = temp_db_path();
42        cleanup(&path);
43        let pager = Pager::open_default(&path).unwrap();
44        (pager, path)
45    }
46
47    #[test]
48    fn inline_raw_round_trip_below_threshold() {
49        let (pager, path) = fresh_pager();
50        let value = vec![0xABu8; OVERFLOW_THRESHOLD - 1];
51        let stored = encode(&pager, &value).unwrap();
52        assert_eq!(
53            reddb_file::decode_btree_value_cell(&stored).unwrap(),
54            reddb_file::BTreeValueCell::Inline {
55                is_compressed: false,
56                payload: value.as_slice(),
57            }
58        );
59        assert_eq!(stored.len(), 1 + value.len());
60        let decoded = decode(&pager, &stored).unwrap();
61        assert_eq!(decoded, value);
62        cleanup(&path);
63    }
64
65    #[test]
66    fn inline_raw_at_exact_threshold() {
67        let (pager, path) = fresh_pager();
68        let value = vec![0x7Eu8; OVERFLOW_THRESHOLD];
69        let stored = encode(&pager, &value).unwrap();
70        assert!(matches!(
71            reddb_file::decode_btree_value_cell(&stored).unwrap(),
72            reddb_file::BTreeValueCell::Inline {
73                is_compressed: false,
74                ..
75            }
76        ));
77        assert_eq!(decode(&pager, &stored).unwrap(), value);
78        cleanup(&path);
79    }
80
81    #[test]
82    fn compressible_above_threshold_inlines_compressed() {
83        let (pager, path) = fresh_pager();
84        // ~44 KB of repeating text — compresses into a few hundred bytes
85        // which fit inline.
86        let value = "the quick brown fox jumps over the lazy dog\n"
87            .repeat(1024)
88            .into_bytes();
89        assert!(value.len() > OVERFLOW_THRESHOLD);
90        let stored = encode(&pager, &value).unwrap();
91        assert!(matches!(
92            reddb_file::decode_btree_value_cell(&stored).unwrap(),
93            reddb_file::BTreeValueCell::Inline {
94                is_compressed: true,
95                ..
96            }
97        ));
98        assert!(
99            stored.len() <= 1 + OVERFLOW_THRESHOLD,
100            "compressed cell must fit inline budget"
101        );
102        let decoded = decode(&pager, &stored).unwrap();
103        assert_eq!(decoded, value);
104        cleanup(&path);
105    }
106
107    #[test]
108    fn incompressible_above_threshold_spills_raw() {
109        let (pager, path) = fresh_pager();
110        // Pseudo-random bytes — incompressible.
111        let mut state: u64 = 0x1234_5678_9ABC_DEF0;
112        let value: Vec<u8> = (0..5 * 1024 * 1024)
113            .map(|_| {
114                state ^= state << 13;
115                state ^= state >> 7;
116                state ^= state << 17;
117                state as u8
118            })
119            .collect();
120        let stored = encode(&pager, &value).unwrap();
121        assert!(matches!(
122            reddb_file::decode_btree_value_cell(&stored).unwrap(),
123            reddb_file::BTreeValueCell::Pointer {
124                is_compressed: false,
125                ..
126            }
127        ));
128        assert_eq!(stored.len(), POINTER_CELL_LEN);
129        let decoded = decode(&pager, &stored).unwrap();
130        assert_eq!(decoded.len(), value.len());
131        assert_eq!(decoded, value);
132        cleanup(&path);
133    }
134
135    #[test]
136    fn value_above_max_rejected_without_allocation() {
137        let (pager, path) = fresh_pager();
138        let before = pager.page_count().unwrap();
139        let value = vec![0u8; MAX_VALUE_SIZE + 1];
140        let err = encode(&pager, &value).unwrap_err();
141        assert!(matches!(err, ValueLayoutError::ValueTooLarge(_)));
142        let after = pager.page_count().unwrap();
143        assert_eq!(before, after, "rejected value must not allocate pages");
144        cleanup(&path);
145    }
146
147    #[test]
148    fn pointer_head_extracts_head_id_only_for_pointer_cells() {
149        let inline = reddb_file::encode_btree_inline_raw(&[1, 2, 3]);
150        assert_eq!(pointer_head(&inline), None);
151        let inline_compressed = reddb_file::encode_btree_inline_compressed(&[0, 0, 0, 5]);
152        assert_eq!(pointer_head(&inline_compressed), None);
153
154        let cell = reddb_file::encode_btree_pointer(0x0102_0304, 0, false);
155        assert_eq!(pointer_head(&cell), Some(0x0102_0304));
156        let compressed_cell = reddb_file::encode_btree_pointer(0x0102_0304, 0, true);
157        assert_eq!(pointer_head(&compressed_cell), Some(0x0102_0304));
158    }
159
160    #[test]
161    fn empty_value_round_trips() {
162        let (pager, path) = fresh_pager();
163        let stored = encode(&pager, &[]).unwrap();
164        assert_eq!(stored, vec![0u8]);
165        assert!(decode(&pager, &stored).unwrap().is_empty());
166        cleanup(&path);
167    }
168}