reddb_server/storage/unified/store/
impl_native_c.rs1use super::*;
2
3impl UnifiedStore {
4 pub fn read_native_physical_state(&self) -> Result<NativePhysicalState, StoreError> {
5 let header = self.physical_file_header().ok_or_else(|| {
6 StoreError::Serialization("native physical header is not available".to_string())
7 })?;
8
9 let collection_roots = self.read_native_collection_roots(header.collection_roots_page)?;
10 let manifest = if header.manifest_page != 0 {
11 self.read_native_manifest_summary(header.manifest_page).ok()
12 } else {
13 None
14 };
15 let registry = if header.registry_page != 0 {
16 self.read_native_registry_summary(header.registry_page).ok()
17 } else {
18 None
19 };
20 let recovery = if header.recovery_page != 0 {
21 self.read_native_recovery_summary(header.recovery_page).ok()
22 } else {
23 None
24 };
25 let catalog = if header.catalog_page != 0 {
26 self.read_native_catalog_summary(header.catalog_page).ok()
27 } else {
28 None
29 };
30 let metadata_state = if header.metadata_state_page != 0 {
31 self.read_native_metadata_state_summary(header.metadata_state_page)
32 .ok()
33 } else {
34 None
35 };
36 let vector_artifact_pages = if header.vector_artifact_page != 0 {
37 self.read_native_vector_artifact_store(header.vector_artifact_page)
38 .ok()
39 } else {
40 None
41 };
42
43 Ok(NativePhysicalState {
44 header,
45 collection_roots,
46 manifest,
47 registry,
48 recovery,
49 catalog,
50 metadata_state,
51 vector_artifact_pages,
52 })
53 }
54
55 fn read_native_blob_chain_page_ids(&self, root_page: u32) -> Result<Vec<u32>, StoreError> {
56 let Some(pager) = &self.pager else {
57 return Err(StoreError::Serialization(
58 "native blob chain requires paged mode".to_string(),
59 ));
60 };
61 if root_page == 0 {
62 return Ok(Vec::new());
63 }
64 let mut pages = Vec::new();
65 let mut current = root_page;
66 while current != 0 {
67 pages.push(current);
68 let page = pager
69 .read_page(current)
70 .map_err(|err| StoreError::Serialization(err.to_string()))?;
71 let bytes = page.as_bytes();
72 let content = &bytes[crate::storage::engine::HEADER_SIZE..];
73 let (next_page, _) = reddb_file::decode_native_blob_page(content)
74 .map_err(|err| StoreError::Serialization(err.to_string()))?;
75 current = next_page;
76 }
77 Ok(pages)
78 }
79
80 fn write_native_blob_chain(
81 &self,
82 payload: &[u8],
83 existing_root: Option<u32>,
84 ) -> Result<(u32, u32, u64), StoreError> {
85 let Some(pager) = &self.pager else {
86 return Ok((0, 0, 0));
87 };
88 if payload.is_empty() {
89 if let Some(root) = existing_root {
90 self.detached_native_blob_pages
91 .lock()
92 .extend(self.read_native_blob_chain_page_ids(root)?);
93 }
94 return Ok((0, 0, 0));
95 }
96
97 let chunk_capacity = reddb_file::native_blob_chunk_capacity(
98 crate::storage::engine::PAGE_SIZE,
99 crate::storage::engine::HEADER_SIZE,
100 );
101 let page_count = payload.len().div_ceil(chunk_capacity) as u32;
102 let mut page_ids = existing_root
103 .map(|root| self.read_native_blob_chain_page_ids(root))
104 .transpose()?
105 .unwrap_or_default();
106 while page_ids.len() < page_count as usize {
107 page_ids.push(
108 pager
109 .allocate_page(crate::storage::engine::PageType::NativeMeta)
110 .map_err(|err| StoreError::Serialization(err.to_string()))?
111 .page_id(),
112 );
113 }
114
115 for (index, chunk) in payload.chunks(chunk_capacity).enumerate() {
116 let page_id = page_ids[index];
117 let next_page = if index + 1 < page_count as usize {
118 page_ids[index + 1]
119 } else {
120 0
121 };
122 let data = reddb_file::encode_native_blob_page(next_page, chunk);
123
124 let mut page = crate::storage::engine::Page::new(
125 crate::storage::engine::PageType::NativeMeta,
126 page_id,
127 );
128 let bytes = page.as_bytes_mut();
129 let content_start = crate::storage::engine::HEADER_SIZE;
130 let copy_len = data.len().min(bytes.len() - content_start);
131 bytes[content_start..content_start + copy_len].copy_from_slice(&data[..copy_len]);
132 pager
133 .write_page(page_id, page)
134 .map_err(|err| StoreError::Serialization(err.to_string()))?;
135 }
136
137 let root_page = page_ids[0];
138 let mut detached = self.detached_native_blob_pages.lock();
139 for page in &page_ids[..page_count as usize] {
142 detached.remove(page);
143 }
144 detached.extend(page_ids.into_iter().skip(page_count as usize));
145
146 Ok((
147 root_page,
148 page_count,
149 crate::storage::engine::crc32(payload) as u64,
150 ))
151 }
152
153 pub fn read_native_blob_chain(&self, root_page: u32) -> Result<Vec<u8>, StoreError> {
154 let _publication = self.vault_publication_lock.lock();
155 let Some(pager) = &self.pager else {
156 return Err(StoreError::Serialization(
157 "native blob chain requires paged mode".to_string(),
158 ));
159 };
160 if root_page == 0 {
161 return Ok(Vec::new());
162 }
163 let mut current = root_page;
164 let mut payload = Vec::new();
165 while current != 0 {
166 let page = pager
167 .read_page(current)
168 .map_err(|err| StoreError::Serialization(err.to_string()))?;
169 let bytes = page.as_bytes();
170 let content = &bytes[crate::storage::engine::HEADER_SIZE..];
171 let (next_page, chunk) = reddb_file::decode_native_blob_page(content)
172 .map_err(|err| StoreError::Serialization(err.to_string()))?;
173 payload.extend_from_slice(&chunk);
174 current = next_page;
175 }
176 Ok(payload)
177 }
178
179 pub fn write_native_vector_artifact_store(
180 &self,
181 artifacts: &[(String, String, Vec<u8>)],
182 existing_page: Option<u32>,
183 ) -> Result<(u32, u64, Vec<NativeVectorArtifactPageSummary>), StoreError> {
184 let _publication = self.vault_publication_lock.lock();
185 let Some(pager) = &self.pager else {
186 return Ok((0, 0, Vec::new()));
187 };
188
189 let existing = existing_page
190 .map(|page| self.read_native_vector_artifact_store(page))
191 .transpose()?
192 .unwrap_or_default();
193 let page_id = match existing_page.filter(|page| *page != 0) {
194 Some(page) => page,
195 None => pager
196 .allocate_page(crate::storage::engine::PageType::NativeMeta)
197 .map_err(|err| StoreError::Serialization(err.to_string()))?
198 .page_id(),
199 };
200
201 let mut summaries = Vec::new();
202 for (collection, artifact_kind, bytes) in artifacts {
203 let existing_root = existing
204 .iter()
205 .find(|entry| {
206 entry.collection == *collection && entry.artifact_kind == *artifact_kind
207 })
208 .map(|entry| entry.root_page);
209 let (root_page, page_count, checksum) =
210 self.write_native_blob_chain(bytes, existing_root)?;
211 summaries.push(NativeVectorArtifactPageSummary {
212 collection: collection.clone(),
213 artifact_kind: artifact_kind.clone(),
214 root_page,
215 page_count,
216 byte_len: bytes.len() as u64,
217 checksum,
218 });
219 }
220
221 for previous in &existing {
222 if !artifacts.iter().any(|(collection, kind, _)| {
223 previous.collection == *collection && previous.artifact_kind == *kind
224 }) {
225 self.detached_native_blob_pages
226 .lock()
227 .extend(self.read_native_blob_chain_page_ids(previous.root_page)?);
228 }
229 }
230
231 let data = reddb_file::encode_native_vector_artifact_store_page(&summaries);
232 let checksum = reddb_file::native_store_page_checksum(&data);
233 let mut page = crate::storage::engine::Page::new(
234 crate::storage::engine::PageType::NativeMeta,
235 page_id,
236 );
237 let bytes = page.as_bytes_mut();
238 let content_start = crate::storage::engine::HEADER_SIZE;
239 let copy_len = data.len().min(bytes.len() - content_start);
240 bytes[content_start..content_start + copy_len].copy_from_slice(&data[..copy_len]);
241 pager
242 .write_page(page_id, page)
243 .map_err(|err| StoreError::Serialization(err.to_string()))?;
244 Ok((page_id, checksum, summaries))
245 }
246
247 pub fn read_native_vector_artifact_store(
248 &self,
249 page_id: u32,
250 ) -> Result<Vec<NativeVectorArtifactPageSummary>, StoreError> {
251 let Some(pager) = &self.pager else {
252 return Err(StoreError::Serialization(
253 "native vector artifact store requires paged mode".to_string(),
254 ));
255 };
256 if page_id == 0 {
257 return Ok(Vec::new());
258 }
259 let page = pager
260 .read_page(page_id)
261 .map_err(|err| StoreError::Serialization(err.to_string()))?;
262 let bytes = page.as_bytes();
263 let content = &bytes[crate::storage::engine::HEADER_SIZE..];
264 reddb_file::decode_native_vector_artifact_store_page(content)
265 .map_err(|err| StoreError::Serialization(err.to_string()))
266 }
267
268 pub fn read_native_vector_artifact_blob(
269 &self,
270 page_id: u32,
271 collection: &str,
272 artifact_kind: Option<&str>,
273 ) -> Result<Option<(NativeVectorArtifactPageSummary, Vec<u8>)>, StoreError> {
274 let _publication = self.vault_publication_lock.lock();
275 let artifact_kind = artifact_kind.unwrap_or("hnsw");
276 let summaries = self.read_native_vector_artifact_store(page_id)?;
277 let Some(summary) = summaries.into_iter().find(|summary| {
278 summary.collection == collection && summary.artifact_kind == artifact_kind
279 }) else {
280 return Ok(None);
281 };
282 let bytes = self.read_native_blob_chain(summary.root_page)?;
283 Ok(Some((summary, bytes)))
284 }
285
286 pub(crate) fn publish_detached_native_blob_pages(&self) -> Result<(), StoreError> {
290 let _publication = self.vault_publication_lock.lock();
291 let Some(pager) = &self.pager else {
292 return Ok(());
293 };
294 let mut detached = self.detached_native_blob_pages.lock();
295 if detached.is_empty() {
296 return Ok(());
297 }
298 while let Some(&page) = detached.first() {
299 pager
300 .free_page(page)
301 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
302 detached.remove(&page);
303 }
304 drop(detached);
305 self.flush_paged_registry()
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn shrinking_native_blob_does_not_read_the_previous_tail() {
317 let directory = tempfile::tempdir().expect("temporary store");
318 let store = UnifiedStore::open(&directory.path().join("blob.rdb")).expect("paged store");
319 let capacity = reddb_file::native_blob_chunk_capacity(
320 crate::storage::engine::PAGE_SIZE,
321 crate::storage::engine::HEADER_SIZE,
322 );
323 let original = vec![7; capacity * 3];
324 let (root, count, _) = store
325 .write_native_blob_chain(&original, None)
326 .expect("original blob");
327 assert_eq!(count, 3);
328 store.persist().expect("publish original blob");
329
330 let replacement = vec![9; capacity / 2];
331 let (root, count, checksum) = store
332 .write_native_blob_chain(&replacement, Some(root))
333 .expect("smaller replacement");
334 assert_eq!(count, 1);
335 let actual = store
336 .read_native_blob_chain(root)
337 .expect("read replacement");
338 assert_eq!(
339 actual.len(),
340 replacement.len(),
341 "old artifact tail survived shrink"
342 );
343 assert_eq!(actual, replacement);
344 assert_eq!(checksum, crate::storage::engine::crc32(&actual) as u64);
345 }
346
347 fn publish_artifacts(store: &UnifiedStore, artifacts: &[(String, String, Vec<u8>)]) -> u32 {
348 let mut header = store.physical_file_header().expect("physical header");
349 let previous = (header.vector_artifact_page != 0).then_some(header.vector_artifact_page);
350 let (page, checksum, _) = store
351 .write_native_vector_artifact_store(artifacts, previous)
352 .expect("write artifact summary");
353 header.vector_artifact_page = page;
354 header.vector_artifact_checksum = checksum;
355 store
356 .update_physical_file_header(header)
357 .expect("new header");
358 store.persist().expect("durable native publication");
359 store
360 .publish_detached_native_blob_pages()
361 .expect("quarantine detached pages");
362 page
363 }
364
365 #[test]
366 fn native_blob_retirement_survives_reopen_and_reuses_pages() {
367 let directory = tempfile::tempdir().expect("temporary store");
368 let path = directory.path().join("retired-blob.rdb");
369 let store = UnifiedStore::open(&path).expect("paged store");
370 let capacity = reddb_file::native_blob_chunk_capacity(
371 crate::storage::engine::PAGE_SIZE,
372 crate::storage::engine::HEADER_SIZE,
373 );
374 let large = ("rows".into(), "text.fulltext".into(), vec![7; capacity * 4]);
375 let small = ("rows".into(), "text.fulltext".into(), vec![9; capacity / 2]);
376 publish_artifacts(&store, std::slice::from_ref(&large));
377 let size = std::fs::metadata(&path).expect("allocated file").len();
378 let page = publish_artifacts(&store, std::slice::from_ref(&small));
379 let (pending, published) = store
380 .pager()
381 .expect("pager")
382 .deferred_reclamation_pages()
383 .expect("quarantine");
384 assert_eq!(pending.len(), 3, "shrunk tail must remain quarantined");
385 assert_eq!(published.len(), 3);
386 drop(store);
387
388 let store = UnifiedStore::open(&path).expect("reopen published quarantine");
389 let (summary, bytes) = store
390 .read_native_vector_artifact_blob(page, "rows", Some("text.fulltext"))
391 .expect("read artifact")
392 .expect("artifact survives");
393 assert_eq!(summary.page_count, 1);
394 assert_eq!(summary.byte_len as usize, bytes.len());
395 assert_eq!(bytes, small.2);
396 store
397 .flush_paged_registry()
398 .expect("release published tail");
399 assert!(
400 store
401 .pager()
402 .expect("pager")
403 .free_page_count()
404 .expect("free pages")
405 >= 2,
406 "one reclaimed page hosts the allocator trunk"
407 );
408 let maximum_size = size + 3 * crate::storage::engine::PAGE_SIZE as u64;
411 let mut plateau = None;
412 for cycle in 0..8 {
413 publish_artifacts(&store, std::slice::from_ref(&large));
414 publish_artifacts(&store, std::slice::from_ref(&small));
415 let current_size = std::fs::metadata(&path).expect("reused allocation").len();
416 assert!(current_size <= maximum_size);
417 let free = store
418 .pager()
419 .expect("pager")
420 .free_page_count()
421 .expect("free pages");
422 if cycle >= 2 {
423 assert_eq!(
424 Some((current_size, free)),
425 plateau,
426 "equal churn must preserve allocation and free-page accounting"
427 );
428 }
429 plateau = Some((current_size, free));
430 }
431 publish_artifacts(&store, &[]);
432 store
433 .flush_paged_registry()
434 .expect("release dropped artifact");
435 assert!(store
436 .read_native_vector_artifact_store(page)
437 .expect("empty summary")
438 .is_empty());
439 assert!(store
440 .pager()
441 .expect("pager")
442 .deferred_reclamation_pages()
443 .expect("released quarantine")
444 .0
445 .is_empty());
446 publish_artifacts(&store, std::slice::from_ref(&large));
449 assert!(std::fs::metadata(&path).expect("reuse after drop").len() <= maximum_size);
450 assert_eq!(
451 store
452 .read_native_vector_artifact_blob(page, "rows", Some("text.fulltext"))
453 .expect("recreated artifact")
454 .expect("artifact exists")
455 .1,
456 large.2
457 );
458 }
459
460 #[test]
461 fn retained_blob_retry_cancels_its_unpublished_retirement() {
462 let directory = tempfile::tempdir().expect("temporary store");
463 let store =
464 UnifiedStore::open(&directory.path().join("retry-blob.rdb")).expect("paged store");
465 let bytes = vec![42; 1024];
466 let (root, _, _) = store
467 .write_native_blob_chain(&bytes, None)
468 .expect("original blob");
469 store
470 .write_native_blob_chain(&[], Some(root))
471 .expect("stage removal");
472 store
473 .write_native_blob_chain(&bytes, Some(root))
474 .expect("retain on retry");
475 store.persist().expect("retry publication");
476 store
477 .publish_detached_native_blob_pages()
478 .expect("no live retirement");
479 store.flush_paged_registry().expect("later publication");
480 assert_eq!(
481 store.read_native_blob_chain(root).expect("retained page"),
482 bytes
483 );
484 assert_eq!(
485 store
486 .pager()
487 .expect("pager")
488 .free_page_count()
489 .expect("no live free"),
490 0
491 );
492 }
493}