nodedb_lite/storage/
pagedb_storage_columnar.rs1#[cfg(not(target_arch = "wasm32"))]
29use async_trait::async_trait;
30#[cfg(not(target_arch = "wasm32"))]
31use pagedb::vfs::Vfs;
32#[cfg(not(target_arch = "wasm32"))]
33use pagedb::{RealmId, SegmentKind};
34
35#[cfg(not(target_arch = "wasm32"))]
36use crate::error::LiteError;
37#[cfg(not(target_arch = "wasm32"))]
38use crate::storage::columnar_segment_ext::ColumnarSegmentExt;
39#[cfg(not(target_arch = "wasm32"))]
40use crate::storage::pagedb_storage::PagedbStorage;
41
42#[cfg(not(target_arch = "wasm32"))]
44const PAGE_BODY_CAP: usize = 4096 - 40;
45
46#[cfg(not(target_arch = "wasm32"))]
48const COL_SEG_PREFIX: &str = "col/seg/";
49
50#[cfg(not(target_arch = "wasm32"))]
51#[async_trait]
52impl<V: Vfs + Clone + Send + Sync + 'static> ColumnarSegmentExt for PagedbStorage<V>
53where
54 <V as Vfs>::LockHandle: Sync,
55 <V as Vfs>::File: Sync,
56{
57 async fn write_columnar_segment(
58 &self,
59 collection: &str,
60 segment_id: u32,
61 bytes: &[u8],
62 ) -> Result<(), LiteError> {
63 let byte_len = bytes.len() as u64;
66 let mut payload = Vec::with_capacity(8 + bytes.len());
67 payload.extend_from_slice(&byte_len.to_le_bytes());
68 payload.extend_from_slice(bytes);
69
70 let chunks: Vec<&[u8]> = payload.chunks(PAGE_BODY_CAP).collect();
71
72 let realm = RealmId::new([0u8; 16]);
73 let segment_name = format!("{COL_SEG_PREFIX}{collection}/{segment_id}");
74
75 let mut writer = self
76 .db
77 .create_segment(realm, SegmentKind::Unspecified)
78 .await
79 .map_err(LiteError::from)?;
80 writer
81 .append_extent(&chunks)
82 .await
83 .map_err(LiteError::from)?;
84 let meta = writer.seal().await.map_err(LiteError::from)?;
85
86 let mut txn = self.db.begin_write().await.map_err(LiteError::from)?;
87
88 let link_result = txn.link_segment(&segment_name, &meta).await;
90 match link_result {
91 Ok(()) => {}
92 Err(pagedb::PagedbError::AlreadyLinked) => {
93 txn.replace_segment(&segment_name, &meta)
94 .await
95 .map_err(LiteError::from)?;
96 }
97 Err(e) => return Err(LiteError::from(e)),
98 }
99
100 txn.commit().await.map(|_| ()).map_err(LiteError::from)
101 }
102
103 async fn open_columnar_segment(
104 &self,
105 collection: &str,
106 segment_id: u32,
107 ) -> Result<Option<Box<[u8]>>, LiteError> {
108 let segment_name = format!("{COL_SEG_PREFIX}{collection}/{segment_id}");
109 let txn = self.db.begin_read().await.map_err(LiteError::from)?;
110
111 let reader = match txn.open_segment(&segment_name).await {
112 Ok(r) => r,
113 Err(pagedb::PagedbError::NotFound) => return Ok(None),
114 Err(e) => return Err(LiteError::from(e)),
115 };
116
117 let meta_page_count = reader.meta().page_count;
119 let index_pages = u64::from(reader.index_page_count());
120 let data_page_count = meta_page_count
121 .checked_sub(2 + index_pages)
122 .ok_or_else(|| LiteError::Storage {
123 detail: format!(
124 "columnar segment '{collection}/{segment_id}' page_count={meta_page_count} \
125 too small (index_pages={index_pages})"
126 ),
127 })?;
128
129 if data_page_count == 0 {
130 return Ok(Some(Box::default()));
131 }
132
133 let count_u32 = u32::try_from(data_page_count).map_err(|_| LiteError::Storage {
134 detail: format!(
135 "columnar segment '{collection}/{segment_id}' has too many data pages: \
136 {data_page_count}"
137 ),
138 })?;
139
140 let pages = reader
141 .read_range(1, count_u32)
142 .await
143 .map_err(|e| LiteError::Storage {
144 detail: format!(
145 "pagedb columnar segment read_range failed for \
146 '{collection}/{segment_id}': {e}"
147 ),
148 })?;
149
150 let total: usize = pages.iter().map(|p| p.len()).sum();
151 let mut flat = Vec::with_capacity(total);
152 for page in pages {
153 flat.extend_from_slice(&page);
154 }
155
156 if flat.len() < 8 {
158 return Err(LiteError::Storage {
159 detail: format!(
160 "columnar segment '{collection}/{segment_id}' too small to contain \
161 length prefix: {} bytes",
162 flat.len()
163 ),
164 });
165 }
166 let byte_len = u64::from_le_bytes(flat[..8].try_into().expect("8-byte slice")) as usize;
167 let end = 8_usize
168 .checked_add(byte_len)
169 .ok_or_else(|| LiteError::Storage {
170 detail: format!(
171 "columnar segment '{collection}/{segment_id}' length prefix overflows: \
172 {byte_len}"
173 ),
174 })?;
175 if end > flat.len() {
176 return Err(LiteError::Storage {
177 detail: format!(
178 "columnar segment '{collection}/{segment_id}' declared byte_len={byte_len} \
179 exceeds available data ({} bytes after prefix)",
180 flat.len() - 8
181 ),
182 });
183 }
184
185 Ok(Some(flat[8..end].to_vec().into_boxed_slice()))
186 }
187
188 async fn delete_columnar_segment(
189 &self,
190 collection: &str,
191 segment_id: u32,
192 ) -> Result<(), LiteError> {
193 let segment_name = format!("{COL_SEG_PREFIX}{collection}/{segment_id}");
194 let mut txn = self.db.begin_write().await.map_err(LiteError::from)?;
195 match txn.unlink_segment(&segment_name).await {
196 Ok(()) => {}
197 Err(pagedb::PagedbError::NotLinked) => {}
198 Err(e) => return Err(LiteError::from(e)),
199 }
200 txn.commit().await.map(|_| ()).map_err(LiteError::from)
201 }
202}
203
204#[cfg(test)]
207mod tests {
208 use crate::storage::columnar_segment_ext::ColumnarSegmentExt;
209 use crate::storage::engine::StorageEngine;
210 use crate::storage::pagedb_storage::PagedbStorage;
211 use pagedb::vfs::memory::MemVfs;
212
213 async fn make_storage() -> PagedbStorage<MemVfs> {
214 PagedbStorage::open_in_memory().await.unwrap()
215 }
216
217 #[tokio::test]
218 async fn columnar_segment_as_columnar_segment_ext_is_some() {
219 let s = make_storage().await;
220 assert!(
221 s.as_columnar_segment_ext().is_some(),
222 "PagedbStorage must return Some from as_columnar_segment_ext"
223 );
224 }
225
226 #[tokio::test]
227 async fn columnar_segment_roundtrip() {
228 let s = make_storage().await;
229 let payload: Vec<u8> = (0u8..=255).cycle().take(5000).collect();
230
231 s.write_columnar_segment("metrics", 1, &payload)
232 .await
233 .unwrap();
234
235 let got = s
236 .open_columnar_segment("metrics", 1)
237 .await
238 .unwrap()
239 .expect("segment must exist after write");
240
241 assert_eq!(
242 got.as_ref(),
243 payload.as_slice(),
244 "round-trip bytes must match"
245 );
246 }
247
248 #[tokio::test]
249 async fn columnar_segment_open_missing_returns_none() {
250 let s = make_storage().await;
251 let result = s.open_columnar_segment("nonexistent", 99).await.unwrap();
252 assert!(result.is_none());
253 }
254
255 #[tokio::test]
256 async fn columnar_segment_delete_removes_segment() {
257 let s = make_storage().await;
258 s.write_columnar_segment("events", 1, b"some column data")
259 .await
260 .unwrap();
261 s.delete_columnar_segment("events", 1).await.unwrap();
262
263 let result = s.open_columnar_segment("events", 1).await.unwrap();
264 assert!(result.is_none(), "segment must be gone after delete");
265 }
266
267 #[tokio::test]
268 async fn columnar_segment_delete_nonexistent_is_noop() {
269 let s = make_storage().await;
270 s.delete_columnar_segment("ghost", 42).await.unwrap();
271 }
272
273 #[tokio::test]
274 async fn columnar_segment_replace_updates_content() {
275 let s = make_storage().await;
276 s.write_columnar_segment("readings", 1, b"original data")
277 .await
278 .unwrap();
279 s.write_columnar_segment("readings", 1, b"updated data")
280 .await
281 .unwrap();
282
283 let got = s
284 .open_columnar_segment("readings", 1)
285 .await
286 .unwrap()
287 .expect("segment must exist");
288 assert_eq!(
289 got.as_ref(),
290 b"updated data",
291 "second write must replace first"
292 );
293 }
294
295 #[tokio::test]
296 async fn columnar_segment_roundtrip_large_payload() {
297 let s = make_storage().await;
298 let payload: Vec<u8> = (0u8..=255).cycle().take(128 * 1024).collect();
300 s.write_columnar_segment("large", 5, &payload)
301 .await
302 .unwrap();
303 let got = s
304 .open_columnar_segment("large", 5)
305 .await
306 .unwrap()
307 .expect("segment must exist");
308 assert_eq!(got.len(), payload.len());
309 assert_eq!(got.as_ref(), payload.as_slice());
310 }
311
312 #[tokio::test]
313 async fn columnar_segment_multiple_collections_isolated() {
314 let s = make_storage().await;
315 s.write_columnar_segment("col_a", 1, b"data for col_a seg 1")
316 .await
317 .unwrap();
318 s.write_columnar_segment("col_b", 1, b"data for col_b seg 1")
319 .await
320 .unwrap();
321 s.write_columnar_segment("col_a", 2, b"data for col_a seg 2")
322 .await
323 .unwrap();
324
325 let a1 = s
326 .open_columnar_segment("col_a", 1)
327 .await
328 .unwrap()
329 .expect("col_a/1 must exist");
330 let b1 = s
331 .open_columnar_segment("col_b", 1)
332 .await
333 .unwrap()
334 .expect("col_b/1 must exist");
335 let a2 = s
336 .open_columnar_segment("col_a", 2)
337 .await
338 .unwrap()
339 .expect("col_a/2 must exist");
340
341 assert_eq!(a1.as_ref(), b"data for col_a seg 1");
342 assert_eq!(b1.as_ref(), b"data for col_b seg 1");
343 assert_eq!(a2.as_ref(), b"data for col_a seg 2");
344 }
345}