nodedb_lite/storage/
pagedb_storage_spatial.rs1#[cfg(not(target_arch = "wasm32"))]
30use async_trait::async_trait;
31#[cfg(not(target_arch = "wasm32"))]
32use pagedb::vfs::Vfs;
33#[cfg(not(target_arch = "wasm32"))]
34use pagedb::{RealmId, SegmentKind};
35
36#[cfg(not(target_arch = "wasm32"))]
37use crate::error::LiteError;
38#[cfg(not(target_arch = "wasm32"))]
39use crate::storage::pagedb_storage::PagedbStorage;
40#[cfg(not(target_arch = "wasm32"))]
41use crate::storage::spatial_segment_ext::SpatialSegmentExt;
42
43#[cfg(not(target_arch = "wasm32"))]
45const PAGE_BODY_CAP: usize = 4096 - 40;
46
47#[cfg(not(target_arch = "wasm32"))]
49const SPATIAL_SEG_PREFIX: &str = "spatial/rtree/";
50
51#[cfg(not(target_arch = "wasm32"))]
57fn segment_name(collection: &str, field: &str) -> String {
58 format!("{SPATIAL_SEG_PREFIX}{collection}/{field}")
59}
60
61#[cfg(not(target_arch = "wasm32"))]
62#[async_trait]
63impl<V: Vfs + Clone + Send + Sync + 'static> SpatialSegmentExt for PagedbStorage<V>
64where
65 <V as Vfs>::LockHandle: Sync,
66 <V as Vfs>::File: Sync,
67{
68 async fn write_spatial_segment(
69 &self,
70 collection: &str,
71 field: &str,
72 bytes: &[u8],
73 ) -> Result<(), LiteError> {
74 let byte_len = bytes.len() as u64;
77 let mut payload = Vec::with_capacity(8 + bytes.len());
78 payload.extend_from_slice(&byte_len.to_le_bytes());
79 payload.extend_from_slice(bytes);
80
81 let chunks: Vec<&[u8]> = payload.chunks(PAGE_BODY_CAP).collect();
82
83 let realm = RealmId::new([0u8; 16]);
84 let name = segment_name(collection, field);
85
86 let mut writer = self
87 .db
88 .create_segment(realm, SegmentKind::Unspecified)
89 .await
90 .map_err(LiteError::from)?;
91 writer
92 .append_extent(&chunks)
93 .await
94 .map_err(LiteError::from)?;
95 let meta = writer.seal().await.map_err(LiteError::from)?;
96
97 let mut txn = self.db.begin_write().await.map_err(LiteError::from)?;
98
99 let link_result = txn.link_segment(&name, &meta).await;
101 match link_result {
102 Ok(()) => {}
103 Err(pagedb::PagedbError::AlreadyLinked) => {
104 txn.replace_segment(&name, &meta)
105 .await
106 .map_err(LiteError::from)?;
107 }
108 Err(e) => return Err(LiteError::from(e)),
109 }
110
111 txn.commit().await.map(|_| ()).map_err(LiteError::from)
112 }
113
114 async fn open_spatial_segment(
115 &self,
116 collection: &str,
117 field: &str,
118 ) -> Result<Option<Box<[u8]>>, LiteError> {
119 let name = segment_name(collection, field);
120 let txn = self.db.begin_read().await.map_err(LiteError::from)?;
121
122 let reader = match txn.open_segment(&name).await {
123 Ok(r) => r,
124 Err(pagedb::PagedbError::NotFound) => return Ok(None),
125 Err(e) => return Err(LiteError::from(e)),
126 };
127
128 let meta_page_count = reader.meta().page_count;
129 let index_pages = u64::from(reader.index_page_count());
130 let data_page_count = meta_page_count
131 .checked_sub(2 + index_pages)
132 .ok_or_else(|| LiteError::Storage {
133 detail: format!(
134 "spatial segment '{name}' page_count={meta_page_count} too small \
135 (index_pages={index_pages})"
136 ),
137 })?;
138
139 if data_page_count == 0 {
140 return Ok(Some(Box::default()));
141 }
142
143 let count_u32 = u32::try_from(data_page_count).map_err(|_| LiteError::Storage {
144 detail: format!("spatial segment '{name}' has too many data pages: {data_page_count}"),
145 })?;
146
147 let pages = reader
148 .read_range(1, count_u32)
149 .await
150 .map_err(|e| LiteError::Storage {
151 detail: format!("pagedb spatial segment read_range failed for '{name}': {e}"),
152 })?;
153
154 let total: usize = pages.iter().map(|p| p.len()).sum();
155 let mut flat = Vec::with_capacity(total);
156 for page in pages {
157 flat.extend_from_slice(&page);
158 }
159
160 if flat.len() < 8 {
162 return Err(LiteError::Storage {
163 detail: format!(
164 "spatial segment '{name}' too small to contain length prefix: {} bytes",
165 flat.len()
166 ),
167 });
168 }
169 let byte_len = u64::from_le_bytes(flat[..8].try_into().expect("8-byte slice")) as usize;
170 let end = 8_usize
171 .checked_add(byte_len)
172 .ok_or_else(|| LiteError::Storage {
173 detail: format!("spatial segment '{name}' length prefix overflows: {byte_len}"),
174 })?;
175 if end > flat.len() {
176 return Err(LiteError::Storage {
177 detail: format!(
178 "spatial segment '{name}' declared byte_len={byte_len} exceeds \
179 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_spatial_segment(&self, collection: &str, field: &str) -> Result<(), LiteError> {
189 let name = segment_name(collection, field);
190 let mut txn = self.db.begin_write().await.map_err(LiteError::from)?;
191 match txn.unlink_segment(&name).await {
192 Ok(()) => {}
193 Err(pagedb::PagedbError::NotLinked) => {}
194 Err(e) => return Err(LiteError::from(e)),
195 }
196 txn.commit().await.map(|_| ()).map_err(LiteError::from)
197 }
198}
199
200#[cfg(test)]
203mod tests {
204 use crate::storage::engine::StorageEngine;
205 use crate::storage::pagedb_storage::PagedbStorage;
206 use crate::storage::spatial_segment_ext::SpatialSegmentExt;
207 use pagedb::vfs::memory::MemVfs;
208
209 async fn make_storage() -> PagedbStorage<MemVfs> {
210 PagedbStorage::open_in_memory().await.unwrap()
211 }
212
213 #[tokio::test]
214 async fn spatial_segment_roundtrip() {
215 let s = make_storage().await;
216 let payload: Vec<u8> = (0u8..=255).cycle().take(5000).collect();
217
218 s.write_spatial_segment("orders", "location", &payload)
219 .await
220 .unwrap();
221
222 let got = s
223 .open_spatial_segment("orders", "location")
224 .await
225 .unwrap()
226 .expect("segment must exist after write");
227
228 assert_eq!(
229 got.as_ref(),
230 payload.as_slice(),
231 "round-trip bytes must match"
232 );
233 }
234
235 #[tokio::test]
236 async fn spatial_segment_open_missing_returns_none() {
237 let s = make_storage().await;
238 let result = s.open_spatial_segment("nonexistent", "geom").await.unwrap();
239 assert!(result.is_none());
240 }
241
242 #[tokio::test]
243 async fn spatial_segment_delete_removes_segment() {
244 let s = make_storage().await;
245 s.write_spatial_segment("col", "field", b"some rtree data")
246 .await
247 .unwrap();
248 s.delete_spatial_segment("col", "field").await.unwrap();
249
250 let result = s.open_spatial_segment("col", "field").await.unwrap();
251 assert!(result.is_none(), "segment must be gone after delete");
252 }
253
254 #[tokio::test]
255 async fn spatial_segment_delete_nonexistent_is_noop() {
256 let s = make_storage().await;
257 s.delete_spatial_segment("ghost", "geom").await.unwrap();
258 }
259
260 #[tokio::test]
261 async fn spatial_segment_replace_updates_content() {
262 let s = make_storage().await;
263 s.write_spatial_segment("col", "field", b"original")
264 .await
265 .unwrap();
266 s.write_spatial_segment("col", "field", b"updated")
267 .await
268 .unwrap();
269
270 let got = s
271 .open_spatial_segment("col", "field")
272 .await
273 .unwrap()
274 .expect("segment must exist");
275 assert_eq!(got.as_ref(), b"updated", "second write must replace first");
276 }
277
278 #[tokio::test]
279 async fn spatial_segment_roundtrip_large_payload() {
280 let s = make_storage().await;
281 let payload: Vec<u8> = (0u8..=255).cycle().take(128 * 1024).collect();
283 s.write_spatial_segment("large_col", "geom", &payload)
284 .await
285 .unwrap();
286 let got = s
287 .open_spatial_segment("large_col", "geom")
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 spatial_segment_multiple_fields_same_collection() {
297 let s = make_storage().await;
298 let payload_a: Vec<u8> = vec![1u8; 100];
299 let payload_b: Vec<u8> = vec![2u8; 200];
300
301 s.write_spatial_segment("places", "point", &payload_a)
302 .await
303 .unwrap();
304 s.write_spatial_segment("places", "polygon", &payload_b)
305 .await
306 .unwrap();
307
308 let got_a = s
309 .open_spatial_segment("places", "point")
310 .await
311 .unwrap()
312 .expect("point segment must exist");
313 let got_b = s
314 .open_spatial_segment("places", "polygon")
315 .await
316 .unwrap()
317 .expect("polygon segment must exist");
318
319 assert_eq!(got_a.as_ref(), payload_a.as_slice());
320 assert_eq!(got_b.as_ref(), payload_b.as_slice());
321 }
322
323 #[tokio::test]
324 async fn spatial_segment_as_spatial_segment_ext_is_some() {
325 let s = make_storage().await;
326 assert!(
327 s.as_spatial_segment_ext().is_some(),
328 "PagedbStorage must return Some from as_spatial_segment_ext"
329 );
330 }
331}