nodedb_lite/storage/
pagedb_storage_graph.rs1#[cfg(not(target_arch = "wasm32"))]
27use async_trait::async_trait;
28#[cfg(not(target_arch = "wasm32"))]
29use nodedb_types::Namespace;
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::graph_segment_ext::GraphSegmentExt;
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 GRAPH_SEG_PREFIX: &str = "graph/csr/";
49
50#[cfg(not(target_arch = "wasm32"))]
52const GRAPH_SEG_IDX_PREFIX: &str = "graph:_seg_idx:";
53
54#[cfg(not(target_arch = "wasm32"))]
55#[async_trait]
56impl<V: Vfs + Clone + Send + Sync + 'static> GraphSegmentExt for PagedbStorage<V>
57where
58 <V as Vfs>::LockHandle: Sync,
59 <V as Vfs>::File: Sync,
60{
61 async fn write_graph_segment(&self, collection: &str, bytes: &[u8]) -> Result<(), LiteError> {
62 let byte_len = bytes.len() as u64;
65 let mut payload = Vec::with_capacity(8 + bytes.len());
66 payload.extend_from_slice(&byte_len.to_le_bytes());
67 payload.extend_from_slice(bytes);
68
69 let chunks: Vec<&[u8]> = payload.chunks(PAGE_BODY_CAP).collect();
70
71 let realm = RealmId::new([0u8; 16]);
72 let segment_name = format!("{GRAPH_SEG_PREFIX}{collection}");
73
74 let mut writer = self
75 .db
76 .create_segment(realm, SegmentKind::Unspecified)
77 .await
78 .map_err(LiteError::from)?;
79 writer
80 .append_extent(&chunks)
81 .await
82 .map_err(LiteError::from)?;
83 let meta = writer.seal().await.map_err(LiteError::from)?;
84
85 let mut txn = self.db.begin_write().await.map_err(LiteError::from)?;
86
87 let link_result = txn.link_segment(&segment_name, &meta).await;
89 match link_result {
90 Ok(()) => {}
91 Err(pagedb::PagedbError::AlreadyLinked) => {
92 txn.replace_segment(&segment_name, &meta)
93 .await
94 .map_err(LiteError::from)?;
95 }
96 Err(e) => return Err(LiteError::from(e)),
97 }
98
99 let sentinel_key = format!("{GRAPH_SEG_IDX_PREFIX}{collection}");
101 txn.put(
102 &crate::storage::pagedb_storage::prefix_key(Namespace::Graph, sentinel_key.as_bytes()),
103 &[],
104 )
105 .await
106 .map_err(LiteError::from)?;
107
108 txn.commit().await.map(|_| ()).map_err(LiteError::from)
109 }
110
111 async fn open_graph_segment(&self, collection: &str) -> Result<Option<Box<[u8]>>, LiteError> {
112 let segment_name = format!("{GRAPH_SEG_PREFIX}{collection}");
113 let txn = self.db.begin_read().await.map_err(LiteError::from)?;
114
115 let reader = match txn.open_segment(&segment_name).await {
116 Ok(r) => r,
117 Err(pagedb::PagedbError::NotFound) => return Ok(None),
118 Err(e) => return Err(LiteError::from(e)),
119 };
120
121 let meta_page_count = reader.meta().page_count;
123 let index_pages = u64::from(reader.index_page_count());
124 let data_page_count = meta_page_count
125 .checked_sub(2 + index_pages)
126 .ok_or_else(|| LiteError::Storage {
127 detail: format!(
128 "graph segment '{collection}' page_count={meta_page_count} too small \
129 (index_pages={index_pages})"
130 ),
131 })?;
132
133 if data_page_count == 0 {
134 return Ok(Some(Box::default()));
135 }
136
137 let count_u32 = u32::try_from(data_page_count).map_err(|_| LiteError::Storage {
138 detail: format!(
139 "graph segment '{collection}' has too many data pages: {data_page_count}"
140 ),
141 })?;
142
143 let pages = reader
144 .read_range(1, count_u32)
145 .await
146 .map_err(|e| LiteError::Storage {
147 detail: format!("pagedb graph segment read_range failed for '{collection}': {e}"),
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 "graph segment '{collection}' too small to contain length prefix: {} bytes",
161 flat.len()
162 ),
163 });
164 }
165 let byte_len = u64::from_le_bytes(flat[..8].try_into().expect("8-byte slice")) as usize;
166 let end = 8_usize
167 .checked_add(byte_len)
168 .ok_or_else(|| LiteError::Storage {
169 detail: format!("graph segment '{collection}' length prefix overflows: {byte_len}"),
170 })?;
171 if end > flat.len() {
172 return Err(LiteError::Storage {
173 detail: format!(
174 "graph segment '{collection}' declared byte_len={byte_len} exceeds \
175 available data ({} bytes after prefix)",
176 flat.len() - 8
177 ),
178 });
179 }
180
181 Ok(Some(flat[8..end].to_vec().into_boxed_slice()))
182 }
183
184 async fn delete_graph_segment(&self, collection: &str) -> Result<(), LiteError> {
185 let segment_name = format!("{GRAPH_SEG_PREFIX}{collection}");
186 let mut txn = self.db.begin_write().await.map_err(LiteError::from)?;
187 match txn.unlink_segment(&segment_name).await {
188 Ok(()) => {}
189 Err(pagedb::PagedbError::NotLinked) => {}
190 Err(e) => return Err(LiteError::from(e)),
191 }
192
193 let sentinel_key = format!("{GRAPH_SEG_IDX_PREFIX}{collection}");
195 txn.delete(&crate::storage::pagedb_storage::prefix_key(
196 Namespace::Graph,
197 sentinel_key.as_bytes(),
198 ))
199 .await
200 .map_err(LiteError::from)?;
201
202 txn.commit().await.map(|_| ()).map_err(LiteError::from)
203 }
204}
205
206#[cfg(test)]
209mod tests {
210 use crate::storage::engine::StorageEngine;
211 use crate::storage::graph_segment_ext::GraphSegmentExt;
212 use crate::storage::pagedb_storage::PagedbStorage;
213 use pagedb::vfs::memory::MemVfs;
214
215 async fn make_storage() -> PagedbStorage<MemVfs> {
216 PagedbStorage::open_in_memory().await.unwrap()
217 }
218
219 #[tokio::test]
220 async fn graph_segment_roundtrip() {
221 let s = make_storage().await;
222 let payload: Vec<u8> = (0u8..=255).cycle().take(5000).collect();
223
224 s.write_graph_segment("social_graph", &payload)
225 .await
226 .unwrap();
227
228 let got = s
229 .open_graph_segment("social_graph")
230 .await
231 .unwrap()
232 .expect("segment must exist after write");
233
234 assert_eq!(
235 got.as_ref(),
236 payload.as_slice(),
237 "round-trip bytes must match"
238 );
239 }
240
241 #[tokio::test]
242 async fn graph_segment_open_missing_returns_none() {
243 let s = make_storage().await;
244 let result = s.open_graph_segment("nonexistent").await.unwrap();
245 assert!(result.is_none());
246 }
247
248 #[tokio::test]
249 async fn graph_segment_delete_removes_segment() {
250 let s = make_storage().await;
251 s.write_graph_segment("col", b"some csr data")
252 .await
253 .unwrap();
254 s.delete_graph_segment("col").await.unwrap();
255
256 let result = s.open_graph_segment("col").await.unwrap();
257 assert!(result.is_none(), "segment must be gone after delete");
258 }
259
260 #[tokio::test]
261 async fn graph_segment_delete_nonexistent_is_noop() {
262 let s = make_storage().await;
263 s.delete_graph_segment("ghost").await.unwrap();
264 }
265
266 #[tokio::test]
267 async fn graph_segment_replace_updates_content() {
268 let s = make_storage().await;
269 s.write_graph_segment("col", b"original").await.unwrap();
270 s.write_graph_segment("col", b"updated").await.unwrap();
271
272 let got = s
273 .open_graph_segment("col")
274 .await
275 .unwrap()
276 .expect("segment must exist");
277 assert_eq!(got.as_ref(), b"updated", "second write must replace first");
278 }
279
280 #[tokio::test]
281 async fn graph_segment_roundtrip_large_payload() {
282 let s = make_storage().await;
283 let payload: Vec<u8> = (0u8..=255).cycle().take(128 * 1024).collect();
285 s.write_graph_segment("large_col", &payload).await.unwrap();
286 let got = s
287 .open_graph_segment("large_col")
288 .await
289 .unwrap()
290 .expect("segment must exist");
291 assert_eq!(got.len(), payload.len());
292 assert_eq!(got.as_ref(), payload.as_slice());
293 }
294
295 #[tokio::test]
296 async fn graph_segment_as_graph_segment_ext_is_some() {
297 let s = make_storage().await;
298 assert!(
299 s.as_graph_segment_ext().is_some(),
300 "PagedbStorage must return Some from as_graph_segment_ext"
301 );
302 }
303}