reddb_server/storage/engine/btree/
value_layout.rs1use 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 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 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}