reddb_engine/storage/btree/
value_layout.rs1use crate::storage::overflow::{OverflowChain, OverflowError};
30use crate::storage::page_store::PageStore;
31use reddb_file::vector_value_codec as value_codec;
32use reddb_file::BTreeValueCell;
33
34pub const OVERFLOW_THRESHOLD: usize = reddb_file::BTREE_VALUE_OVERFLOW_THRESHOLD;
37
38pub const COMPRESSION_THRESHOLD: usize = reddb_file::BTREE_VALUE_COMPRESSION_THRESHOLD;
40
41pub const MAX_VALUE_SIZE: usize = reddb_file::BTREE_VALUE_MAX_SIZE;
45
46pub const POINTER_CELL_LEN: usize = reddb_file::BTREE_VALUE_POINTER_CELL_LEN;
48
49#[derive(Debug)]
51pub enum ValueLayoutError {
52 ValueTooLarge(usize),
54 UnknownFlag(u8),
56 TruncatedPointer { got: usize },
58 Codec(value_codec::ValueCodecError),
60 Overflow(OverflowError),
62 ReadLimitExceeded {
64 required_bytes: usize,
65 limit_bytes: usize,
66 },
67}
68
69impl std::fmt::Display for ValueLayoutError {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 match self {
72 Self::ValueTooLarge(n) => {
73 write!(f, "value too large: {} bytes (max {})", n, MAX_VALUE_SIZE)
74 }
75 Self::UnknownFlag(b) => write!(f, "unknown leaf-cell flag byte: 0b{:08b}", b),
76 Self::TruncatedPointer { got } => {
77 write!(
78 f,
79 "pointer cell truncated: need {} bytes after flag, got {}",
80 POINTER_CELL_LEN - 1,
81 got
82 )
83 }
84 Self::Codec(e) => write!(f, "value codec: {}", e),
85 Self::Overflow(e) => write!(f, "overflow chain: {}", e),
86 Self::ReadLimitExceeded {
87 required_bytes,
88 limit_bytes,
89 } => write!(
90 f,
91 "read needs {required_bytes} bytes over read memory limit {limit_bytes}"
92 ),
93 }
94 }
95}
96
97impl std::error::Error for ValueLayoutError {}
98
99impl From<value_codec::ValueCodecError> for ValueLayoutError {
100 fn from(e: value_codec::ValueCodecError) -> Self {
101 Self::Codec(e)
102 }
103}
104
105impl From<reddb_file::BTreeValueCellError> for ValueLayoutError {
106 fn from(e: reddb_file::BTreeValueCellError) -> Self {
107 match e {
108 reddb_file::BTreeValueCellError::UnknownFlag(flag) => Self::UnknownFlag(flag),
109 reddb_file::BTreeValueCellError::TruncatedPointer { got } => {
110 Self::TruncatedPointer { got }
111 }
112 }
113 }
114}
115
116impl From<OverflowError> for ValueLayoutError {
117 fn from(e: OverflowError) -> Self {
118 Self::Overflow(e)
119 }
120}
121
122pub(crate) fn encode_inline_without_pages(
124 value: &[u8],
125) -> Result<Option<Vec<u8>>, ValueLayoutError> {
126 if value.len() > MAX_VALUE_SIZE {
127 return Err(ValueLayoutError::ValueTooLarge(value.len()));
128 }
129 if value.len() <= COMPRESSION_THRESHOLD {
130 return Ok(Some(reddb_file::encode_btree_inline_raw(value)));
131 }
132 let (flag, bytes) = value_codec::encode(value);
133 if bytes.len() <= OVERFLOW_THRESHOLD {
134 return Ok(Some(match flag {
135 value_codec::ValueFlag::Raw => reddb_file::encode_btree_inline_raw(&bytes),
136 value_codec::ValueFlag::Lz4 => reddb_file::encode_btree_inline_compressed(&bytes),
137 }));
138 }
139 Ok(None)
140}
141
142pub fn encode<P: PageStore>(pager: &P, value: &[u8]) -> Result<Vec<u8>, ValueLayoutError> {
146 if value.len() > MAX_VALUE_SIZE {
147 return Err(ValueLayoutError::ValueTooLarge(value.len()));
148 }
149
150 if value.len() <= COMPRESSION_THRESHOLD {
154 return Ok(reddb_file::encode_btree_inline_raw(value));
155 }
156
157 let (codec_flag, codec_bytes) = value_codec::encode(value);
160
161 if codec_bytes.len() <= OVERFLOW_THRESHOLD {
164 return Ok(match codec_flag {
165 value_codec::ValueFlag::Raw => reddb_file::encode_btree_inline_raw(&codec_bytes),
166 value_codec::ValueFlag::Lz4 => reddb_file::encode_btree_inline_compressed(&codec_bytes),
167 });
168 }
169
170 let is_compressed = codec_flag == value_codec::ValueFlag::Lz4;
175 let chain = OverflowChain::new(pager);
176 let (head, total_len) = chain.store(&codec_bytes)?;
177
178 Ok(reddb_file::encode_btree_pointer(
179 head,
180 total_len,
181 is_compressed,
182 ))
183}
184
185pub fn decode<P: PageStore>(pager: &P, stored: &[u8]) -> Result<Vec<u8>, ValueLayoutError> {
188 decode_with_budget(pager, stored, usize::MAX)
189}
190
191pub fn decode_with_budget<P: PageStore>(
194 pager: &P,
195 stored: &[u8],
196 limit_bytes: usize,
197) -> Result<Vec<u8>, ValueLayoutError> {
198 match reddb_file::decode_btree_value_cell(stored)? {
199 BTreeValueCell::Pointer {
200 is_compressed,
201 head_page_id,
202 total_len,
203 } => {
204 let encoded_bytes = usize::try_from(total_len)
205 .map_err(|_| ValueLayoutError::ValueTooLarge(usize::MAX))?;
206 check_read_budget(encoded_bytes, limit_bytes)?;
207 let chain = OverflowChain::new(pager);
208 let chain_bytes = chain.read(head_page_id, total_len)?;
209 if is_compressed {
210 let decoded_bytes = compressed_output_bytes(&chain_bytes)?;
211 check_read_budget(encoded_bytes.saturating_add(decoded_bytes), limit_bytes)?;
212 Ok(value_codec::decode(
213 value_codec::ValueFlag::Lz4,
214 &chain_bytes,
215 )?)
216 } else {
217 Ok(chain_bytes)
218 }
219 }
220 BTreeValueCell::Inline {
221 is_compressed,
222 payload,
223 } => {
224 let decoded_bytes = if is_compressed {
225 compressed_output_bytes(payload)?
226 } else {
227 payload.len()
228 };
229 check_read_budget(decoded_bytes, limit_bytes)?;
230 Ok(reddb_file::decode_btree_inline_payload(
231 is_compressed,
232 payload,
233 )?)
234 }
235 }
236}
237
238fn compressed_output_bytes(bytes: &[u8]) -> Result<usize, ValueLayoutError> {
239 value_codec::decoded_length(value_codec::ValueFlag::Lz4, bytes).map_err(ValueLayoutError::Codec)
240}
241
242fn check_read_budget(required_bytes: usize, limit_bytes: usize) -> Result<(), ValueLayoutError> {
243 if required_bytes > limit_bytes {
244 return Err(ValueLayoutError::ReadLimitExceeded {
245 required_bytes,
246 limit_bytes,
247 });
248 }
249 Ok(())
250}
251
252pub fn pointer_head(stored: &[u8]) -> Option<u32> {
258 reddb_file::btree_value_pointer_head(stored)
259}
260
261#[inline]
265#[allow(dead_code)]
266pub fn projected_cell_len(input: &[u8]) -> usize {
267 let codec_len = value_codec::would_encode_to(input);
268 reddb_file::btree_projected_cell_len(input.len(), codec_len)
269}