1use super::*;
2use crate::storage::unified::metadata::{MetadataFilter, MetadataValue};
3
4const PHYSICAL_METADATA_KEY: &str = "physical";
5
6#[cfg(test)]
7fn metadata_qualification_phase(started: &std::time::Instant, name: &str) {
8 if std::env::var_os("REDDB_QUALIFICATION_CHECKPOINT_PHASES").is_some() {
9 eprintln!(
10 "runtime qualification phase=checkpoint_metadata name={name} elapsed_us={}",
11 started.elapsed().as_micros()
12 );
13 }
14}
15
16impl RedDB {
17 pub(crate) fn persist_physical_metadata_record(
20 &self,
21 metadata: &PhysicalMetadataFile,
22 ) -> Result<(), Box<dyn std::error::Error>> {
23 if self.options.read_only {
24 return Err("physical metadata operations are not allowed in read-only mode".into());
25 }
26 let value = crate::serde_json::Value::String(metadata.to_document_json(false)?);
27 self.store
28 .try_set_storage_metadata(PHYSICAL_METADATA_KEY, &value)?;
29 Ok(())
30 }
31
32 fn load_internal_physical_metadata(
33 &self,
34 ) -> Result<Option<PhysicalMetadataFile>, Box<dyn std::error::Error>> {
35 let Some(value) = self.store.try_get_storage_metadata(PHYSICAL_METADATA_KEY)? else {
36 return Ok(None);
37 };
38 let Value::Text(document) = value else {
39 return Err(std::io::Error::new(
40 std::io::ErrorKind::InvalidData,
41 "invalid internal physical metadata record",
42 )
43 .into());
44 };
45 Ok(Some(PhysicalMetadataFile::from_document_json(
46 &document,
47 "invalid internal physical metadata record",
48 )?))
49 }
50
51 fn has_legacy_metadata_sidecars(&self) -> std::io::Result<bool> {
52 let Some(path) = self.path() else {
53 return Ok(false);
54 };
55 Ok(
56 PhysicalMetadataFile::metadata_binary_path_for(path).try_exists()?
57 || PhysicalMetadataFile::metadata_path_for(path).try_exists()?
58 || !PhysicalMetadataFile::journal_paths_for_data_path(path)?.is_empty(),
59 )
60 }
61
62 pub(crate) fn remove_imported_metadata_sidecars(
65 &self,
66 path: &Path,
67 ) -> Result<(), Box<dyn std::error::Error>> {
68 if self.options.read_only
69 || !self.has_legacy_metadata_sidecars()?
70 || self.load_internal_physical_metadata()?.is_none()
71 {
72 return Ok(());
73 }
74 reddb_file::physical_metadata::remove_imported_physical_metadata_sidecars(path)?;
75 Ok(())
76 }
77
78 pub fn is_replica_role(&self) -> bool {
79 matches!(
80 self.options.replication.role,
81 crate::replication::ReplicationRole::Replica { .. }
82 )
83 }
84
85 pub fn enforce_retention_policy(&self) -> Result<(), Box<dyn std::error::Error>> {
86 if self.options.read_only || self.is_replica_role() {
87 return Ok(());
88 }
89
90 if self.options.mode == StorageMode::Persistent {
93 let Some(_) = self.path() else {
94 return Ok(());
95 };
96
97 let _publication = self.store.vault_publication_lock.lock();
98 let Ok(mut metadata) = self.load_or_bootstrap_physical_metadata(true) else {
99 return Ok(());
100 };
101
102 self.prune_export_registry(&mut metadata.exports);
103 self.persist_physical_metadata_record(&metadata)?;
104 }
105
106 let _ = self.sweep_ttl_expired_entities()?;
107
108 Ok(())
109 }
110
111 pub(crate) fn ttl_expired_entities_now(
112 &self,
113 ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
114 self.ttl_expired_entities_at(current_unix_ms())
115 }
116
117 pub fn replica_allows_entity_at_read(
118 &self,
119 collection: &str,
120 entity: &crate::storage::UnifiedEntity,
121 ) -> bool {
122 if !self.is_replica_role() {
123 return true;
124 }
125 !self.entity_expired_at(collection, entity, current_unix_ms())
126 }
127
128 fn sweep_ttl_expired_entities(&self) -> Result<usize, Box<dyn std::error::Error>> {
129 let to_delete = self.ttl_expired_entities_now()?;
130
131 let mut deleted = 0usize;
132 for (collection, id) in to_delete {
133 match self.store.delete(&collection, id) {
134 Ok(true) => deleted = deleted.saturating_add(1),
135 Ok(false) => {}
136 Err(err) => {
137 return Err(format!(
138 "failed deleting expired entity {id} from collection '{collection}': {err:?}"
139 )
140 .into());
141 }
142 }
143 }
144
145 Ok(deleted)
146 }
147
148 fn ttl_expired_entities_at(
149 &self,
150 now_ms: u64,
151 ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
152 let mut to_delete = Vec::<(String, EntityId)>::new();
153
154 let mut absolute_expired = self.expired_entities_by_expires_at(now_ms)?;
155 to_delete.append(&mut absolute_expired);
156
157 let mut relative_expired = self.expired_entities_by_ttl(now_ms)?;
158 to_delete.append(&mut relative_expired);
159
160 to_delete.sort_unstable();
161 to_delete.dedup();
162
163 Ok(to_delete)
164 }
165
166 fn entity_expired_at(
167 &self,
168 collection: &str,
169 entity: &crate::storage::UnifiedEntity,
170 now_ms: u64,
171 ) -> bool {
172 let Some(metadata) = self.store.get_metadata(collection, entity.id) else {
173 return false;
174 };
175
176 Self::entity_metadata_expired_at(entity, &metadata, now_ms)
177 }
178
179 pub(crate) fn entity_metadata_expired_at(
180 entity: &crate::storage::UnifiedEntity,
181 metadata: &crate::storage::unified::metadata::Metadata,
182 now_ms: u64,
183 ) -> bool {
184 if metadata
185 .get("_expires_at")
186 .and_then(Self::metadata_u64)
187 .is_some_and(|expires_at_ms| expires_at_ms <= now_ms)
188 {
189 return true;
190 }
191
192 let ttl_ms = metadata.get("_ttl_ms").and_then(Self::metadata_u64);
193 let ttl_secs = if ttl_ms.is_none() {
194 metadata.get("_ttl").and_then(|value| {
195 Self::metadata_u64(value).and_then(|value_secs| value_secs.checked_mul(1000))
196 })
197 } else {
198 None
199 };
200
201 let Some(ttl_ms) = ttl_ms.or(ttl_secs) else {
202 return false;
203 };
204 entity
205 .created_at
206 .saturating_mul(1000)
207 .saturating_add(ttl_ms)
208 <= now_ms
209 }
210
211 fn expired_entities_by_expires_at(
212 &self,
213 now_ms: u64,
214 ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
215 let mut ids = self.store.filter_metadata_all(&[(
216 "_expires_at".to_string(),
217 MetadataFilter::Le(MetadataValue::Timestamp(now_ms)),
218 )]);
219
220 if let Ok(now_ms_i64) = i64::try_from(now_ms) {
221 ids.extend(self.store.filter_metadata_all(&[(
222 "_expires_at".to_string(),
223 MetadataFilter::Le(MetadataValue::Int(now_ms_i64)),
224 )]));
225 }
226
227 let now_ms_f64 = now_ms as f64;
228 if now_ms_f64.is_finite() {
229 ids.extend(self.store.filter_metadata_all(&[(
230 "_expires_at".to_string(),
231 MetadataFilter::Le(MetadataValue::Float(now_ms_f64)),
232 )]));
233 }
234
235 Ok(ids)
236 }
237
238 fn expired_entities_by_ttl(
239 &self,
240 now_ms: u64,
241 ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
242 let mut candidates = Vec::<(String, EntityId)>::new();
243
244 let ttl_ms_candidates = self
245 .store
246 .filter_metadata_all(&[("_ttl_ms".to_string(), MetadataFilter::IsNotNull)]);
247 candidates.extend(ttl_ms_candidates);
248
249 let ttl_candidates = self
250 .store
251 .filter_metadata_all(&[("_ttl".to_string(), MetadataFilter::IsNotNull)]);
252 candidates.extend(ttl_candidates);
253
254 if candidates.is_empty() {
255 return Ok(Vec::new());
256 }
257
258 candidates.sort_unstable();
259 candidates.dedup();
260
261 let mut expired = Vec::<(String, EntityId)>::new();
262 for (collection, entity_id) in candidates {
263 let Some(entity) = self.store.get(&collection, entity_id) else {
264 continue;
265 };
266
267 let Some(metadata) = self.store.get_metadata(&collection, entity_id) else {
268 continue;
269 };
270
271 let ttl_ms = metadata.get("_ttl_ms").and_then(Self::metadata_u64);
272 let ttl_secs = if ttl_ms.is_none() {
273 metadata.get("_ttl").and_then(|value| {
274 Self::metadata_u64(value).and_then(|value_secs| value_secs.checked_mul(1000))
275 })
276 } else {
277 None
278 };
279
280 let Some(ttl_ms) = ttl_ms.or(ttl_secs) else {
281 continue;
282 };
283
284 let created_at_ms = entity.created_at.saturating_mul(1000);
285 let expiry_ms = created_at_ms.saturating_add(ttl_ms);
286 if expiry_ms <= now_ms {
287 expired.push((collection, entity_id));
288 }
289 }
290
291 Ok(expired)
292 }
293
294 fn metadata_u64(value: &MetadataValue) -> Option<u64> {
295 match value {
296 MetadataValue::Int(v) if *v >= 0 => Some(*v as u64),
297 MetadataValue::Timestamp(v) => Some(*v),
298 MetadataValue::Float(v) => {
299 if !v.is_finite() || !v.is_sign_positive() || v.fract().abs() >= f64::EPSILON {
300 return None;
301 }
302 if *v > u64::MAX as f64 {
303 return None;
304 }
305 Some(v.trunc() as u64)
306 }
307 MetadataValue::String(v) => v.parse::<u64>().ok(),
308 _ => None,
309 }
310 }
311
312 pub fn node(&self, collection: impl Into<String>, label: impl Into<String>) -> NodeBuilder {
325 NodeBuilder::new(
326 self.store.clone(),
327 self.preprocessors.clone(),
328 collection,
329 label,
330 )
331 }
332
333 pub fn edge(&self, collection: impl Into<String>, label: impl Into<String>) -> EdgeBuilder {
345 EdgeBuilder::new(
346 self.store.clone(),
347 self.preprocessors.clone(),
348 collection,
349 label,
350 )
351 }
352
353 pub fn vector(&self, collection: impl Into<String>) -> VectorBuilder {
364 VectorBuilder::new(self.store.clone(), self.preprocessors.clone(), collection)
365 }
366
367 pub fn row(&self, table: impl Into<String>, columns: Vec<(&str, Value)>) -> RowBuilder {
378 RowBuilder::new(
379 self.store.clone(),
380 self.preprocessors.clone(),
381 table,
382 columns,
383 )
384 }
385
386 pub fn doc(&self, collection: impl Into<String>) -> DocumentBuilder {
400 DocumentBuilder::new(self.store.clone(), self.preprocessors.clone(), collection)
401 }
402
403 pub fn kv(
414 &self,
415 collection: impl Into<String>,
416 key: impl Into<String>,
417 value: Value,
418 ) -> KvBuilder {
419 KvBuilder::new(
420 self.store.clone(),
421 self.preprocessors.clone(),
422 collection,
423 key,
424 value,
425 )
426 }
427
428 pub fn get_kv(&self, collection: &str, key: &str) -> Option<(Value, EntityId)> {
433 let manager = self.store.get_collection(collection)?;
434 if let Some(value) = manager.project_key_entity(key, None, false, |entity| {
435 (
436 entity
437 .data
438 .as_row()
439 .and_then(|row| row.get_field("value"))
440 .cloned()
441 .unwrap_or(Value::Null),
442 entity.id,
443 )
444 }) {
445 return Some(value);
446 }
447 let columns = manager.column_schema();
448 let key_index = columns
449 .as_ref()
450 .and_then(|names| names.iter().position(|name| name == "key"));
451 let value_index = columns
452 .as_ref()
453 .and_then(|names| names.iter().position(|name| name == "value"));
454 let mut found = None;
455 manager.for_each_entity(|entity| {
456 let EntityData::Row(row) = &entity.data else {
457 return true;
458 };
459 let legacy = row.named.is_none() && row.schema.is_none();
460 let stored_key = row.get_field("key").or_else(|| {
461 if legacy {
462 key_index.and_then(|index| row.columns.get(index))
463 } else {
464 None
465 }
466 });
467 if matches!(stored_key, Some(Value::Text(stored)) if stored.as_ref() == key) {
468 let value = row.get_field("value").or_else(|| {
469 if legacy {
470 value_index.and_then(|index| row.columns.get(index))
471 } else {
472 None
473 }
474 });
475 found = Some((value.cloned().unwrap_or(Value::Null), entity.id));
476 return false;
477 }
478 true
479 });
480 found
481 }
482
483 pub fn delete_kv(
485 &self,
486 collection: &str,
487 key: &str,
488 ) -> Result<bool, super::super::error::DevXError> {
489 let Some((_, id)) = self.get_kv(collection, key) else {
490 return Ok(false);
491 };
492 self.store
493 .delete(collection, id)
494 .map_err(|err| super::super::error::DevXError::Storage(format!("{err:?}")))?;
495 Ok(true)
496 }
497
498 pub(crate) fn with_initialized_metadata(self) -> Result<Self, Box<dyn std::error::Error>> {
499 if self.options.mode == StorageMode::Persistent {
500 let has_internal_metadata = self.load_internal_physical_metadata()?.is_some();
502 match self.load_or_bootstrap_physical_metadata(false) {
504 Ok(metadata) => {
505 crate::reserved_fields::validate_physical_metadata_contracts(&metadata)
506 .map_err(|err| err.to_string())?;
507 if !has_internal_metadata && !self.options.read_only {
508 self.persist_physical_metadata_record(&metadata)?;
509 }
510 }
511 Err(error)
512 if error
513 .downcast_ref::<std::io::Error>()
514 .is_some_and(crate::physical::is_collection_contract_decode_error) =>
515 {
516 return Err(error);
517 }
518 Err(error) => {
519 if self.has_legacy_metadata_sidecars()? {
520 return Err(error);
521 }
522 }
523 }
524 }
528 self.load_collection_ttl_defaults_from_metadata();
529 if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
536 && self.options.storage_profile.packaging
537 == crate::storage::StoragePackaging::SingleFile
538 && !self.paged_mode
539 {
540 self.seed_contract_cache_from_store_aux()?;
541 }
542 self.load_hypertables_from_metadata();
546 Ok(self)
550 }
551
552 pub(crate) fn persist_metadata(&self) -> Result<(), Box<dyn std::error::Error>> {
553 if self.options.mode != StorageMode::Persistent || self.options.read_only {
554 return Ok(());
555 }
556 if !self.paged_mode
559 && self.options.storage_profile.deploy_profile
560 == crate::storage::DeployProfile::Embedded
561 && self.options.storage_profile.packaging
562 == crate::storage::StoragePackaging::SingleFile
563 {
564 return Ok(());
565 }
566 let Some(_) = self.path() else {
567 return Ok(());
568 };
569
570 #[cfg(test)]
571 let qualification_started = std::time::Instant::now();
572 let _publication = self.store.vault_publication_lock.lock();
573 #[cfg(test)]
574 metadata_qualification_phase(&qualification_started, "publication-lock");
575 let previous = self.load_or_bootstrap_physical_metadata(false).ok();
576 #[cfg(test)]
577 metadata_qualification_phase(&qualification_started, "previous-metadata");
578 let collection_roots = self.physical_collection_roots();
579 #[cfg(test)]
580 metadata_qualification_phase(&qualification_started, "collection-roots");
581 let native_state = self.native_physical_state();
582 #[cfg(test)]
583 metadata_qualification_phase(&qualification_started, "native-state");
584 let mut indexes = native_state
585 .map(|state| self.physical_index_state_from_native_state(&state, previous.as_ref()))
586 .unwrap_or_else(|| self.physical_index_state());
587 if let Some(previous) = previous.as_ref() {
588 let absent = |name: &str| {
592 !collection_roots.contains_key(name)
593 && !previous
594 .collection_contracts
595 .iter()
596 .any(|contract| contract.name == name)
597 };
598 let mut removed: BTreeSet<&str> = previous
599 .superblock
600 .collection_roots
601 .keys()
602 .map(String::as_str)
603 .filter(|name| absent(name))
604 .collect();
605 let mut seen = BTreeSet::new();
606 for event in previous.manifest_events.iter().rev() {
607 if seen.insert(event.collection.as_str())
608 && matches!(&event.kind, crate::physical::ManifestEventKind::Remove)
609 && absent(&event.collection)
610 {
611 removed.insert(event.collection.as_str());
612 }
613 }
614 indexes.retain(|index| {
615 !index
616 .collection
617 .as_deref()
618 .is_some_and(|collection| removed.contains(collection))
619 });
620 }
621 #[cfg(test)]
622 metadata_qualification_phase(&qualification_started, "index-state");
623 let mut metadata = PhysicalMetadataFile::from_state(
624 self.options.clone(),
625 self.catalog_snapshot(),
626 collection_roots,
627 indexes,
628 previous.as_ref(),
629 );
630 metadata.collection_ttl_defaults_ms = self.collection_ttl_defaults_snapshot();
631 metadata.hypertables = self.hypertable_registry_snapshot();
635 #[cfg(test)]
636 metadata_qualification_phase(&qualification_started, "metadata-assembled");
637 self.persist_native_physical_header(&metadata)?;
638 #[cfg(test)]
639 metadata_qualification_phase(&qualification_started, "native-header");
640 Ok(())
641 }
642
643 fn bootstrap_metadata_from_native_state(&self) -> Result<bool, Box<dyn std::error::Error>> {
644 if self.options.mode != StorageMode::Persistent || self.options.read_only {
645 return Ok(false);
646 }
647 let Some(_) = self.path() else {
648 return Ok(false);
649 };
650 let Some(native_state) = self.native_physical_state() else {
651 return Ok(false);
652 };
653 if !Self::native_state_is_bootstrap_complete(&native_state) {
654 return Ok(false);
655 }
656
657 let _publication = self.store.vault_publication_lock.lock();
658 let previous = self.load_or_bootstrap_physical_metadata(false).ok();
659 let metadata = self.metadata_from_native_state(&native_state, previous.as_ref());
660 self.persist_native_physical_header(&metadata)?;
661 Ok(true)
662 }
663
664 pub fn rebuild_physical_metadata_from_native_state(
667 &self,
668 ) -> Result<bool, Box<dyn std::error::Error>> {
669 self.bootstrap_metadata_from_native_state()
670 }
671
672 pub(crate) fn native_state_is_bootstrap_complete(native_state: &NativePhysicalState) -> bool {
673 let registry_complete = native_state.registry.as_ref().map(|registry| {
674 registry.collections_complete
675 && registry.indexes_complete
676 && registry.graph_projections_complete
677 && registry.analytics_jobs_complete
678 && registry.vector_artifacts_complete
679 });
680 let recovery_complete = native_state
681 .recovery
682 .as_ref()
683 .map(|recovery| recovery.snapshots_complete && recovery.exports_complete);
684 let catalog_complete = native_state
685 .catalog
686 .as_ref()
687 .map(|catalog| catalog.collections_complete);
688
689 registry_complete == Some(true)
690 && recovery_complete == Some(true)
691 && catalog_complete == Some(true)
692 }
693
694 fn native_state_is_summary_free_legacy(native_state: &NativePhysicalState) -> bool {
695 let header = native_state.header;
696 header.manifest_page == 0
697 && header.manifest_checksum == 0
698 && header.registry_page == 0
699 && header.registry_checksum == 0
700 && header.recovery_page == 0
701 && header.recovery_checksum == 0
702 && header.catalog_page == 0
703 && header.catalog_checksum == 0
704 && header.metadata_state_page == 0
705 && header.metadata_state_checksum == 0
706 && header.vector_artifact_page == 0
707 && header.vector_artifact_checksum == 0
708 && native_state.manifest.is_none()
709 && native_state.registry.is_none()
710 && native_state.recovery.is_none()
711 && native_state.catalog.is_none()
712 && native_state.metadata_state.is_none()
713 && native_state.vector_artifact_pages.is_none()
714 }
715
716 pub(crate) fn load_or_bootstrap_physical_metadata(
717 &self,
718 persist_bootstrapped: bool,
719 ) -> Result<PhysicalMetadataFile, Box<dyn std::error::Error>> {
720 if self.options.mode != StorageMode::Persistent {
721 return Err("physical metadata requires persistent mode".into());
722 }
723 let Some(path) = self.path() else {
724 return Err("database path is not available".into());
725 };
726 let _publication = self.store.vault_publication_lock.lock();
727 let internal_metadata = match self.load_internal_physical_metadata()? {
728 Some(metadata) => {
729 let native_ahead = self.store.physical_file_header().is_some_and(|header| {
733 Self::repair_policy_for_inspection(
734 &Self::inspect_native_header_against_metadata(header, &metadata),
735 ) == NativeHeaderRepairPolicy::NativeAheadOfMetadata
736 });
737 if !native_ahead {
738 return Ok(metadata);
739 }
740 Some(metadata)
741 }
742 None => None,
743 };
744 let native_state = self.native_physical_state();
745 let metadata = match internal_metadata {
746 Some(metadata) => Ok(metadata),
747 None => PhysicalMetadataFile::inspect_for_data_path(path),
748 };
749 match metadata {
750 Ok(metadata) => {
751 if let Some(native_state) = native_state.as_ref() {
752 let inspection = Self::inspect_native_header_against_metadata(
753 native_state.header,
754 &metadata,
755 );
756 if Self::repair_policy_for_inspection(&inspection)
757 == NativeHeaderRepairPolicy::NativeAheadOfMetadata
758 {
759 let bootstrapped =
760 self.metadata_from_native_state(native_state, Some(&metadata));
761 if persist_bootstrapped && !self.options.read_only {
762 self.persist_native_physical_header(&bootstrapped)?;
763 }
764 return Ok(bootstrapped);
765 }
766 }
767 Ok(metadata)
768 }
769 Err(err) => {
770 if crate::physical::is_collection_contract_decode_error(&err)
771 || self.has_legacy_metadata_sidecars()?
772 {
773 return Err(err.into());
774 }
775 let Some(native_state) = native_state else {
776 return Err(err.into());
777 };
778 if !Self::native_state_is_summary_free_legacy(&native_state)
785 && !Self::native_state_is_bootstrap_complete(&native_state)
786 {
787 return Err(err.into());
788 }
789 let metadata = self.metadata_from_native_state(&native_state, None);
790 if persist_bootstrapped && !self.options.read_only {
791 self.persist_native_physical_header(&metadata)?;
792 }
793 Ok(metadata)
794 }
795 }
796 }
797
798 pub(crate) fn physical_metadata_preference(&self) -> Option<&'static str> {
799 if self
800 .load_internal_physical_metadata()
801 .ok()
802 .flatten()
803 .is_some()
804 {
805 return Some("internal_record");
806 }
807 let path = self.path()?;
808 let native_state = self.native_physical_state();
809 let metadata = PhysicalMetadataFile::inspect_for_data_path(path).ok();
810
811 match (metadata, native_state) {
812 (Some(metadata), Some(native_state)) => {
813 let inspection =
814 Self::inspect_native_header_against_metadata(native_state.header, &metadata);
815 match Self::repair_policy_for_inspection(&inspection) {
816 NativeHeaderRepairPolicy::InSync => Some("sidecar_current"),
817 NativeHeaderRepairPolicy::RepairNativeFromMetadata => Some("sidecar_current"),
818 NativeHeaderRepairPolicy::NativeAheadOfMetadata => Some("native_ahead"),
819 }
820 }
821 (Some(_), None) => Some("sidecar_only"),
822 (None, Some(_)) => Some("sidecar_missing_native_available"),
823 (None, None) => Some("sidecar_missing_no_native"),
824 }
825 }
826
827 fn metadata_from_native_state(
828 &self,
829 native_state: &NativePhysicalState,
830 previous: Option<&PhysicalMetadataFile>,
831 ) -> PhysicalMetadataFile {
832 let now = SystemTime::now()
833 .duration_since(UNIX_EPOCH)
834 .unwrap_or_default()
835 .as_millis();
836 let catalog = self.catalog_snapshot();
837 let catalog_name = catalog.name.clone();
838 let catalog_total_entities = catalog.total_entities;
839 let catalog_total_collections = catalog.total_collections;
840 let indexes = self.physical_index_state_from_native_state(native_state, previous);
841
842 let mut manifest =
843 crate::api::SchemaManifest::now(self.options.clone(), catalog.total_collections);
844 manifest.updated_at_unix_ms = now;
845
846 let manifest_events = native_state
847 .manifest
848 .as_ref()
849 .map(|summary| {
850 summary
851 .recent_events
852 .iter()
853 .map(|event| crate::physical::ManifestEvent {
854 collection: event.collection.clone(),
855 object_key: event.object_key.clone(),
856 kind: match event.kind.as_str() {
857 "insert" => crate::physical::ManifestEventKind::Insert,
858 "update" => crate::physical::ManifestEventKind::Update,
859 "remove" => crate::physical::ManifestEventKind::Remove,
860 _ => crate::physical::ManifestEventKind::Checkpoint,
861 },
862 block: crate::physical::BlockReference {
863 index: event.block_index,
864 checksum: event.block_checksum,
865 },
866 snapshot_min: event.snapshot_min,
867 snapshot_max: event.snapshot_max,
868 })
869 .collect()
870 })
871 .unwrap_or_default();
872
873 let graph_projections = native_state
874 .registry
875 .as_ref()
876 .and_then(|registry| {
877 registry.graph_projections_complete.then(|| {
878 registry
879 .graph_projections
880 .iter()
881 .map(|projection| crate::physical::PhysicalGraphProjection {
882 name: projection.name.clone(),
883 created_at_unix_ms: projection.created_at_unix_ms,
884 updated_at_unix_ms: projection.updated_at_unix_ms,
885 state: "materialized".to_string(),
886 source: projection.source.clone(),
887 node_labels: projection.node_labels.clone(),
888 node_types: projection.node_types.clone(),
889 edge_labels: projection.edge_labels.clone(),
890 last_materialized_sequence: projection.last_materialized_sequence,
891 })
892 .collect()
893 })
894 })
895 .or_else(|| previous.map(|metadata| metadata.graph_projections.clone()))
896 .unwrap_or_default();
897
898 let analytics_jobs = native_state
899 .registry
900 .as_ref()
901 .and_then(|registry| {
902 registry.analytics_jobs_complete.then(|| {
903 registry
904 .analytics_jobs
905 .iter()
906 .map(|job| crate::physical::PhysicalAnalyticsJob {
907 id: job.id.clone(),
908 kind: job.kind.clone(),
909 state: job.state.clone(),
910 projection: job.projection.clone(),
911 created_at_unix_ms: job.created_at_unix_ms,
912 updated_at_unix_ms: job.updated_at_unix_ms,
913 last_run_sequence: job.last_run_sequence,
914 metadata: job.metadata.clone(),
915 })
916 .collect()
917 })
918 })
919 .or_else(|| previous.map(|metadata| metadata.analytics_jobs.clone()))
920 .unwrap_or_default();
921
922 let exports = native_state
923 .recovery
924 .as_ref()
925 .and_then(|recovery| {
926 recovery.exports_complete.then(|| {
927 recovery
928 .exports
929 .iter()
930 .map(|export| crate::physical::ExportDescriptor {
931 name: export.name.clone(),
932 created_at_unix_ms: export.created_at_unix_ms,
933 snapshot_id: export.snapshot_id,
934 superblock_sequence: export.superblock_sequence,
935 data_path: self
936 .path()
937 .map(|path| {
938 crate::physical::PhysicalMetadataFile::export_data_path_for(
939 path,
940 &export.name,
941 )
942 .display()
943 .to_string()
944 })
945 .unwrap_or_default(),
946 metadata_path: self
947 .path()
948 .map(|path| {
949 let export_data_path =
950 crate::physical::PhysicalMetadataFile::export_data_path_for(
951 path,
952 &export.name,
953 );
954 crate::physical::PhysicalMetadataFile::metadata_path_for(
955 &export_data_path,
956 )
957 .display()
958 .to_string()
959 })
960 .unwrap_or_default(),
961 collection_count: export.collection_count as usize,
962 total_entities: export.total_entities as usize,
963 })
964 .collect()
965 })
966 })
967 .or_else(|| previous.map(|metadata| metadata.exports.clone()))
968 .unwrap_or_default();
969
970 let snapshots = native_state
971 .recovery
972 .as_ref()
973 .and_then(|recovery| {
974 recovery.snapshots_complete.then(|| {
975 recovery
976 .snapshots
977 .iter()
978 .map(|snapshot| crate::physical::SnapshotDescriptor {
979 snapshot_id: snapshot.snapshot_id,
980 created_at_unix_ms: snapshot.created_at_unix_ms,
981 superblock_sequence: snapshot.superblock_sequence,
982 collection_count: snapshot.collection_count as usize,
983 total_entities: snapshot.total_entities as usize,
984 })
985 .collect()
986 })
987 })
988 .or_else(|| previous.map(|metadata| metadata.snapshots.clone()))
989 .unwrap_or_else(|| {
990 vec![crate::physical::SnapshotDescriptor {
991 snapshot_id: native_state.header.sequence,
992 created_at_unix_ms: now,
993 superblock_sequence: native_state.header.sequence,
994 collection_count: catalog_total_collections,
995 total_entities: catalog_total_entities,
996 }]
997 });
998
999 let catalog_stats = native_state
1000 .catalog
1001 .as_ref()
1002 .and_then(|native_catalog| {
1003 native_catalog.collections_complete.then(|| {
1004 native_catalog
1005 .collections
1006 .iter()
1007 .map(|collection| {
1008 (
1009 collection.name.clone(),
1010 crate::api::CollectionStats {
1011 entities: collection.entities as usize,
1012 cross_refs: collection.cross_refs as usize,
1013 segments: collection.segments as usize,
1014 },
1015 )
1016 })
1017 .collect::<BTreeMap<_, _>>()
1018 })
1019 })
1020 .or_else(|| previous.map(|metadata| metadata.catalog.stats_by_collection.clone()))
1021 .unwrap_or_else(|| catalog.stats_by_collection.clone());
1022
1023 PhysicalMetadataFile {
1024 protocol_version: crate::physical::PHYSICAL_METADATA_PROTOCOL_VERSION.to_string(),
1025 generated_at_unix_ms: now,
1026 last_loaded_from: Some("native_bootstrap".to_string()),
1027 last_healed_at_unix_ms: Some(now),
1028 manifest,
1029 catalog: crate::api::CatalogSnapshot {
1030 name: catalog_name,
1031 total_entities: native_state
1032 .catalog
1033 .as_ref()
1034 .map(|summary| summary.total_entities as usize)
1035 .unwrap_or(catalog_total_entities),
1036 total_collections: native_state
1037 .catalog
1038 .as_ref()
1039 .map(|summary| summary.collection_count as usize)
1040 .unwrap_or(catalog_total_collections),
1041 stats_by_collection: catalog_stats,
1042 updated_at: SystemTime::now(),
1043 },
1044 manifest_events,
1045 collection_ttl_defaults_ms: previous
1046 .map(|metadata| metadata.collection_ttl_defaults_ms.clone())
1047 .unwrap_or_default(),
1048 collection_contracts: previous
1049 .map(|metadata| metadata.collection_contracts.clone())
1050 .unwrap_or_default(),
1051 hypertables: previous
1052 .map(|metadata| metadata.hypertables.clone())
1053 .unwrap_or_default(),
1054 tree_definitions: previous
1055 .map(|metadata| metadata.tree_definitions.clone())
1056 .unwrap_or_default(),
1057 indexes,
1058 graph_projections,
1059 analytics_jobs,
1060 exports,
1061 superblock: crate::physical::SuperblockHeader {
1062 format_version: native_state.header.format_version,
1063 sequence: native_state.header.sequence,
1064 copies: crate::physical::DEFAULT_SUPERBLOCK_COPIES,
1065 manifest: crate::physical::ManifestPointers {
1066 oldest: crate::physical::BlockReference {
1067 index: native_state.header.manifest_oldest_root,
1068 checksum: 0,
1069 },
1070 newest: crate::physical::BlockReference {
1071 index: native_state.header.manifest_root,
1072 checksum: 0,
1073 },
1074 },
1075 free_set: crate::physical::BlockReference {
1076 index: native_state.header.free_set_root,
1077 checksum: 0,
1078 },
1079 collection_roots: native_state.collection_roots.clone(),
1080 },
1081 snapshots,
1082 }
1083 }
1084
1085 pub(crate) fn reconcile_index_states_with_native_artifacts(
1086 &self,
1087 mut indexes: Vec<PhysicalIndexState>,
1088 ) -> Vec<PhysicalIndexState> {
1089 let native_state = self.native_physical_state();
1090 let native_artifacts = native_state
1091 .as_ref()
1092 .and_then(|state| state.registry.as_ref())
1093 .map(|registry| registry.vector_artifacts.as_slice())
1094 .unwrap_or_default();
1095 let artifact_pages = native_state
1096 .as_ref()
1097 .and_then(|state| state.vector_artifact_pages.as_ref());
1098 for index in &mut indexes {
1099 let Some(collection) = index.collection.as_deref() else {
1100 continue;
1101 };
1102 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
1103 continue;
1104 };
1105 let Some(artifact) = native_artifacts.iter().find(|artifact| {
1106 artifact.collection == collection && artifact.artifact_kind == artifact_kind
1107 }) else {
1108 index.build_state = if index.backend == "memory-budget-deferred" {
1109 "deferred-memory-budget"
1110 } else {
1111 "metadata-only"
1112 }
1113 .to_string();
1114 continue;
1115 };
1116 index.entries = artifact.vector_count as usize;
1117 index.estimated_memory_bytes = artifact.serialized_bytes;
1118 index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
1119 index.artifact_kind = Some(artifact.artifact_kind.clone());
1120 index.artifact_checksum = Some(artifact.checksum);
1121 index.build_state = "artifact-published".to_string();
1122 if let Some(pages) = artifact_pages {
1123 index.artifact_root_page = pages
1124 .iter()
1125 .find(|page| {
1126 page.collection == artifact.collection
1127 && page.artifact_kind == artifact.artifact_kind
1128 })
1129 .map(|page| page.root_page);
1130 }
1131 }
1132 indexes
1133 }
1134
1135 pub(crate) fn warmup_native_vector_artifact_for_index(
1136 &self,
1137 index: &PhysicalIndexState,
1138 ) -> Result<(), String> {
1139 let Some(collection) = index.collection.as_deref() else {
1140 return Ok(());
1141 };
1142 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
1143 return Ok(());
1144 };
1145 self.warmup_native_vector_artifact(collection, Some(artifact_kind))?;
1146 Ok(())
1147 }
1148
1149 pub(crate) fn apply_runtime_native_artifact_to_index_state(
1150 &self,
1151 index: &mut PhysicalIndexState,
1152 ) -> Result<(), String> {
1153 let Some(collection) = index.collection.as_deref() else {
1154 return Ok(());
1155 };
1156 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
1157 return Ok(());
1158 };
1159 let artifact = self.inspect_native_vector_artifact(collection, Some(artifact_kind))?;
1160 index.entries = artifact
1161 .graph_edge_count
1162 .or(artifact.text_posting_count)
1163 .unwrap_or(artifact.node_count) as usize;
1164 index.estimated_memory_bytes = artifact.byte_len;
1165 index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
1166 index.artifact_kind = Some(artifact.artifact_kind.clone());
1167 index.artifact_checksum = Some(artifact.checksum);
1168 index.build_state = "ready".to_string();
1169 index.artifact_root_page = self
1170 .native_vector_artifact_pages()
1171 .and_then(|pages| {
1172 pages.into_iter().find(|page| {
1173 page.collection == artifact.collection
1174 && page.artifact_kind == artifact.artifact_kind
1175 })
1176 })
1177 .map(|page| page.root_page);
1178 Ok(())
1179 }
1180
1181 pub(crate) fn physical_index_state_from_native_state(
1182 &self,
1183 native_state: &NativePhysicalState,
1184 previous: Option<&PhysicalMetadataFile>,
1185 ) -> Vec<PhysicalIndexState> {
1186 let mut fresh = self.physical_index_state();
1187 let Some(registry) = native_state.registry.as_ref() else {
1188 if let Some(previous) = previous {
1189 for index in &previous.indexes {
1190 if !fresh.iter().any(|candidate| candidate.name == index.name) {
1191 fresh.push(index.clone());
1192 }
1193 }
1194 }
1195 return fresh;
1196 };
1197
1198 for index in &mut fresh {
1199 if let Some(native) = registry
1200 .indexes
1201 .iter()
1202 .find(|candidate| candidate.name == index.name)
1203 {
1204 index.enabled = native.enabled;
1205 index.last_refresh_ms = native.last_refresh_ms;
1206 index.backend = native.backend.clone();
1207 index.entries = native.entries as usize;
1208 index.estimated_memory_bytes = native.estimated_memory_bytes;
1209 if index.artifact_kind.is_none() {
1210 index.artifact_kind = Self::native_artifact_kind_for_index(index.kind)
1211 .map(|value| value.to_string());
1212 }
1213 if index.build_state == "catalog-derived" {
1214 index.build_state = "registry-loaded".to_string();
1215 }
1216 }
1217 if index.backend == "memory-budget-deferred"
1218 || previous.is_some_and(|metadata| {
1219 metadata.indexes.iter().any(|previous| {
1220 previous.name == index.name && previous.backend == "memory-budget-deferred"
1221 })
1222 })
1223 {
1224 index.backend = "memory-budget-deferred".to_string();
1225 index.build_state = "deferred-memory-budget".to_string();
1226 index.artifact_root_page = None;
1227 index.artifact_checksum = None;
1228 }
1229 }
1230
1231 for native in ®istry.indexes {
1232 if fresh.iter().any(|index| index.name == native.name) {
1233 continue;
1234 }
1235 let Some(kind) = Self::index_kind_from_str(&native.kind) else {
1236 continue;
1237 };
1238 fresh.push(PhysicalIndexState {
1239 name: native.name.clone(),
1240 kind,
1241 collection: native.collection.clone(),
1242 enabled: native.enabled,
1243 entries: native.entries as usize,
1244 estimated_memory_bytes: native.estimated_memory_bytes,
1245 last_refresh_ms: native.last_refresh_ms,
1246 backend: native.backend.clone(),
1247 artifact_kind: Self::native_artifact_kind_for_index(kind)
1248 .map(|value| value.to_string()),
1249 artifact_root_page: None,
1250 artifact_checksum: None,
1251 build_state: "registry-loaded".to_string(),
1252 });
1253 }
1254
1255 if !registry.indexes_complete {
1256 if let Some(previous) = previous {
1257 for index in &previous.indexes {
1258 if !fresh.iter().any(|candidate| candidate.name == index.name) {
1259 fresh.push(index.clone());
1260 }
1261 }
1262 }
1263 }
1264
1265 fresh
1266 }
1267
1268 pub(crate) fn graph_projections_from_native_state(
1269 &self,
1270 native_state: &NativePhysicalState,
1271 ) -> Vec<PhysicalGraphProjection> {
1272 native_state
1273 .registry
1274 .as_ref()
1275 .map(|registry| {
1276 registry
1277 .graph_projections
1278 .iter()
1279 .map(|projection| PhysicalGraphProjection {
1280 name: projection.name.clone(),
1281 created_at_unix_ms: projection.created_at_unix_ms,
1282 updated_at_unix_ms: projection.updated_at_unix_ms,
1283 state: "materialized".to_string(),
1284 source: projection.source.clone(),
1285 node_labels: projection.node_labels.clone(),
1286 node_types: projection.node_types.clone(),
1287 edge_labels: projection.edge_labels.clone(),
1288 last_materialized_sequence: projection.last_materialized_sequence,
1289 })
1290 .collect()
1291 })
1292 .unwrap_or_default()
1293 }
1294
1295 pub(crate) fn analytics_jobs_from_native_state(
1296 &self,
1297 native_state: &NativePhysicalState,
1298 ) -> Vec<PhysicalAnalyticsJob> {
1299 native_state
1300 .registry
1301 .as_ref()
1302 .map(|registry| {
1303 registry
1304 .analytics_jobs
1305 .iter()
1306 .map(|job| PhysicalAnalyticsJob {
1307 id: job.id.clone(),
1308 kind: job.kind.clone(),
1309 state: job.state.clone(),
1310 projection: job.projection.clone(),
1311 created_at_unix_ms: job.created_at_unix_ms,
1312 updated_at_unix_ms: job.updated_at_unix_ms,
1313 last_run_sequence: job.last_run_sequence,
1314 metadata: job.metadata.clone(),
1315 })
1316 .collect()
1317 })
1318 .unwrap_or_default()
1319 }
1320
1321 pub(crate) fn exports_from_native_state(
1322 &self,
1323 native_state: &NativePhysicalState,
1324 ) -> Vec<ExportDescriptor> {
1325 native_state
1326 .recovery
1327 .as_ref()
1328 .map(|recovery| {
1329 recovery
1330 .exports
1331 .iter()
1332 .map(|export| ExportDescriptor {
1333 name: export.name.clone(),
1334 created_at_unix_ms: export.created_at_unix_ms,
1335 snapshot_id: export.snapshot_id,
1336 superblock_sequence: export.superblock_sequence,
1337 data_path: self
1338 .path()
1339 .map(|path| {
1340 crate::physical::PhysicalMetadataFile::export_data_path_for(
1341 path,
1342 &export.name,
1343 )
1344 .display()
1345 .to_string()
1346 })
1347 .unwrap_or_default(),
1348 metadata_path: self
1349 .path()
1350 .map(|path| {
1351 let export_data_path =
1352 crate::physical::PhysicalMetadataFile::export_data_path_for(
1353 path,
1354 &export.name,
1355 );
1356 crate::physical::PhysicalMetadataFile::metadata_path_for(
1357 &export_data_path,
1358 )
1359 .display()
1360 .to_string()
1361 })
1362 .unwrap_or_default(),
1363 collection_count: export.collection_count as usize,
1364 total_entities: export.total_entities as usize,
1365 })
1366 .collect()
1367 })
1368 .unwrap_or_default()
1369 }
1370
1371 pub(crate) fn snapshots_from_native_state(
1372 &self,
1373 native_state: &NativePhysicalState,
1374 ) -> Vec<crate::physical::SnapshotDescriptor> {
1375 let snapshots: Vec<_> = native_state
1376 .recovery
1377 .as_ref()
1378 .map(|recovery| {
1379 recovery
1380 .snapshots
1381 .iter()
1382 .map(|snapshot| crate::physical::SnapshotDescriptor {
1383 snapshot_id: snapshot.snapshot_id,
1384 created_at_unix_ms: snapshot.created_at_unix_ms,
1385 superblock_sequence: snapshot.superblock_sequence,
1386 collection_count: snapshot.collection_count as usize,
1387 total_entities: snapshot.total_entities as usize,
1388 })
1389 .collect()
1390 })
1391 .unwrap_or_default();
1392 if !snapshots.is_empty() {
1393 return snapshots;
1394 }
1395
1396 let now = SystemTime::now()
1397 .duration_since(UNIX_EPOCH)
1398 .unwrap_or_default()
1399 .as_millis();
1400 let (collection_count, total_entities) = native_state
1401 .catalog
1402 .as_ref()
1403 .map(|catalog| {
1404 (
1405 catalog.collection_count as usize,
1406 catalog.total_entities as usize,
1407 )
1408 })
1409 .unwrap_or_else(|| {
1410 let catalog = self.catalog_snapshot();
1411 (catalog.total_collections, catalog.total_entities)
1412 });
1413
1414 vec![crate::physical::SnapshotDescriptor {
1415 snapshot_id: native_state.header.sequence,
1416 created_at_unix_ms: now,
1417 superblock_sequence: native_state.header.sequence,
1418 collection_count,
1419 total_entities,
1420 }]
1421 }
1422
1423 fn index_kind_from_str(value: &str) -> Option<crate::index::IndexKind> {
1424 match value {
1425 "btree" => Some(crate::index::IndexKind::BTree),
1426 "vector.hnsw" => Some(crate::index::IndexKind::VectorHnsw),
1427 "vector.inverted" => Some(crate::index::IndexKind::VectorInverted),
1428 "vector.turbo" => Some(crate::index::IndexKind::VectorTurbo),
1429 "graph.adjacency" => Some(crate::index::IndexKind::GraphAdjacency),
1430 "text.fulltext" => Some(crate::index::IndexKind::FullText),
1431 "document.pathvalue" => Some(crate::index::IndexKind::DocumentPathValue),
1432 "search.hybrid" => Some(crate::index::IndexKind::HybridSearch),
1433 _ => None,
1434 }
1435 }
1436
1437 pub(crate) fn native_artifact_kind_for_index(kind: IndexKind) -> Option<&'static str> {
1438 match kind {
1439 IndexKind::VectorHnsw => Some("hnsw"),
1440 IndexKind::VectorInverted => Some("ivf"),
1441 IndexKind::VectorTurbo => Some("turboquant"),
1442 IndexKind::GraphAdjacency => Some("graph.adjacency"),
1443 IndexKind::FullText => Some("text.fulltext"),
1444 IndexKind::DocumentPathValue => Some("document.pathvalue"),
1445 _ => None,
1446 }
1447 }
1448
1449 fn index_is_declared(&self, name: &str) -> bool {
1450 self.physical_metadata()
1451 .map(|metadata| metadata.indexes.iter().any(|index| index.name == name))
1452 .unwrap_or(false)
1453 }
1454
1455 pub(crate) fn graph_projection_is_declared(&self, name: &str) -> bool {
1456 self.physical_metadata()
1457 .map(|metadata| {
1458 metadata
1459 .graph_projections
1460 .iter()
1461 .any(|projection| projection.name == name)
1462 })
1463 .unwrap_or(false)
1464 }
1465
1466 pub(crate) fn graph_projection_is_operational(&self, name: &str) -> bool {
1467 self.operational_graph_projections()
1468 .into_iter()
1469 .any(|projection| projection.name == name && projection.state == "materialized")
1470 }
1471
1472 pub(crate) fn analytics_job_id(kind: &str, projection: Option<&str>) -> String {
1473 match projection {
1474 Some(projection) => format!("{kind}::{projection}"),
1475 None => format!("{kind}::global"),
1476 }
1477 }
1478
1479 pub(crate) fn update_physical_metadata<T, F>(
1480 &self,
1481 mutator: F,
1482 ) -> Result<T, Box<dyn std::error::Error>>
1483 where
1484 F: FnOnce(&mut PhysicalMetadataFile) -> T,
1485 {
1486 if self.options.mode != StorageMode::Persistent {
1487 return Err("physical metadata operations require persistent mode".into());
1488 }
1489 if self.options.read_only {
1490 return Err("physical metadata operations are not allowed in read-only mode".into());
1491 }
1492 let Some(_) = self.path() else {
1493 return Err("database path is not available".into());
1494 };
1495
1496 let _publication = self.store.vault_publication_lock.lock();
1503 let mut metadata = self.load_or_bootstrap_physical_metadata(true)?;
1504
1505 if metadata.indexes.is_empty() {
1506 metadata.indexes = self
1507 .native_physical_state()
1508 .map(|state| self.physical_index_state_from_native_state(&state, Some(&metadata)))
1509 .unwrap_or_else(|| self.physical_index_state());
1510 }
1511 metadata.superblock.collection_roots = self.physical_collection_roots();
1512
1513 let result = mutator(&mut metadata);
1514 self.persist_native_physical_header(&metadata)?;
1515 Ok(result)
1516 }
1517
1518 pub(crate) fn persist_native_physical_header(
1519 &self,
1520 metadata: &PhysicalMetadataFile,
1521 ) -> Result<(), Box<dyn std::error::Error>> {
1522 if !self.paged_mode {
1523 return self.persist_physical_metadata_record(metadata);
1524 }
1525
1526 let _publication = self.store.vault_publication_lock.lock();
1527 let existing_page = self
1528 .store
1529 .physical_file_header()
1530 .map(|header| header.collection_roots_page)
1531 .filter(|page| *page != 0);
1532 let existing_registry_page = self
1533 .store
1534 .physical_file_header()
1535 .map(|header| header.registry_page)
1536 .filter(|page| *page != 0);
1537 let existing_recovery_page = self
1538 .store
1539 .physical_file_header()
1540 .map(|header| header.recovery_page)
1541 .filter(|page| *page != 0);
1542 let existing_catalog_page = self
1543 .store
1544 .physical_file_header()
1545 .map(|header| header.catalog_page)
1546 .filter(|page| *page != 0);
1547 let existing_metadata_state_page = self
1548 .store
1549 .physical_file_header()
1550 .map(|header| header.metadata_state_page)
1551 .filter(|page| *page != 0);
1552 let existing_vector_artifact_page = self
1553 .store
1554 .physical_file_header()
1555 .map(|header| header.vector_artifact_page)
1556 .filter(|page| *page != 0);
1557 let existing_manifest_page = self
1558 .store
1559 .physical_file_header()
1560 .map(|header| header.manifest_page)
1561 .filter(|page| *page != 0);
1562 let (manifest_page, manifest_checksum) = self.store.write_native_manifest_summary(
1563 metadata.superblock.sequence,
1564 &metadata.manifest_events,
1565 existing_manifest_page,
1566 )?;
1567 let (collection_roots_page, collection_roots_checksum) = self
1568 .store
1569 .write_native_collection_roots(&metadata.superblock.collection_roots, existing_page)?;
1570 let build = self.native_vector_artifact_records()?;
1573 let vector_artifact_records = build.records;
1574 let mut metadata = metadata.clone();
1575 for index in &mut metadata.indexes {
1576 let Some(kind) = Self::native_artifact_kind_for_index(index.kind) else {
1577 continue;
1578 };
1579 let Some(collection) = &index.collection else {
1580 continue;
1581 };
1582 if build.deferred_collections.contains(collection) {
1583 index.backend = "memory-budget-deferred".to_string();
1584 index.build_state = "deferred-memory-budget".to_string();
1585 index.artifact_kind = Some(kind.to_string());
1586 index.artifact_root_page = None;
1587 index.artifact_checksum = None;
1588 } else if let Some((summary, _)) =
1589 vector_artifact_records.iter().find(|(summary, _)| {
1590 &summary.collection == collection && summary.artifact_kind == kind
1591 })
1592 {
1593 index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
1594 index.build_state = "artifact-published".to_string();
1595 index.artifact_kind = Some(kind.to_string());
1596 index.artifact_checksum = Some(summary.checksum);
1597 }
1598 }
1599 let metadata = &metadata;
1600 let artifact_summaries = vector_artifact_records
1601 .iter()
1602 .map(|(summary, _)| summary.clone())
1603 .collect::<Vec<_>>();
1604 let registry_summary = self.native_registry_summary_from_metadata(
1605 metadata,
1606 &artifact_summaries,
1607 artifact_summaries.len(),
1608 );
1609 let (registry_page, registry_checksum) = self
1610 .store
1611 .write_native_registry_summary(®istry_summary, existing_registry_page)?;
1612 let recovery_summary = Self::native_recovery_summary_from_metadata(metadata);
1613 let (recovery_page, recovery_checksum) = self
1614 .store
1615 .write_native_recovery_summary(&recovery_summary, existing_recovery_page)?;
1616 let catalog_summary = Self::native_catalog_summary_from_metadata(metadata);
1617 let (catalog_page, catalog_checksum) = self
1618 .store
1619 .write_native_catalog_summary(&catalog_summary, existing_catalog_page)?;
1620 let metadata_state_summary = Self::native_metadata_state_summary_from_metadata(metadata);
1621 let (metadata_state_page, metadata_state_checksum) =
1622 self.store.write_native_metadata_state_summary(
1623 &metadata_state_summary,
1624 existing_metadata_state_page,
1625 )?;
1626 let vector_artifact_payloads = vector_artifact_records
1627 .iter()
1628 .map(|(summary, bytes)| {
1629 (
1630 summary.collection.clone(),
1631 summary.artifact_kind.clone(),
1632 bytes.clone(),
1633 )
1634 })
1635 .collect::<Vec<_>>();
1636 let (vector_artifact_page, vector_artifact_checksum, _vector_artifact_pages) =
1637 self.store.write_native_vector_artifact_store(
1638 &vector_artifact_payloads,
1639 existing_vector_artifact_page,
1640 )?;
1641 let mut header = Self::native_header_from_metadata(metadata);
1642 header.manifest_page = manifest_page;
1643 header.manifest_checksum = manifest_checksum;
1644 header.collection_roots_page = collection_roots_page;
1645 header.collection_roots_checksum = collection_roots_checksum;
1646 header.registry_page = registry_page;
1647 header.registry_checksum = registry_checksum;
1648 header.recovery_page = recovery_page;
1649 header.recovery_checksum = recovery_checksum;
1650 header.catalog_page = catalog_page;
1651 header.catalog_checksum = catalog_checksum;
1652 header.metadata_state_page = metadata_state_page;
1653 header.metadata_state_checksum = metadata_state_checksum;
1654 header.vector_artifact_page = vector_artifact_page;
1655 header.vector_artifact_checksum = vector_artifact_checksum;
1656 self.persist_physical_metadata_record(metadata)?;
1657 self.store.update_physical_file_header(header)?;
1658 self.store.persist()?;
1659 self.store.publish_detached_native_blob_pages()?;
1660 Ok(())
1661 }
1662
1663 pub(crate) fn native_header_from_metadata(
1664 metadata: &PhysicalMetadataFile,
1665 ) -> PhysicalFileHeader {
1666 PhysicalFileHeader {
1667 format_version: metadata.superblock.format_version,
1668 sequence: metadata.superblock.sequence,
1669 manifest_oldest_root: metadata.superblock.manifest.oldest.index,
1670 manifest_root: metadata.superblock.manifest.newest.index,
1671 free_set_root: metadata.superblock.free_set.index,
1672 manifest_page: 0,
1673 manifest_checksum: 0,
1674 collection_roots_page: 0,
1675 collection_roots_checksum: 0,
1676 collection_root_count: metadata.superblock.collection_roots.len() as u32,
1677 snapshot_count: metadata.snapshots.len() as u32,
1678 index_count: metadata.indexes.len() as u32,
1679 catalog_collection_count: metadata.catalog.total_collections as u32,
1680 catalog_total_entities: metadata.catalog.total_entities as u64,
1681 export_count: metadata.exports.len() as u32,
1682 graph_projection_count: metadata.graph_projections.len() as u32,
1683 analytics_job_count: metadata.analytics_jobs.len() as u32,
1684 manifest_event_count: metadata.manifest_events.len() as u32,
1685 registry_page: 0,
1686 registry_checksum: 0,
1687 recovery_page: 0,
1688 recovery_checksum: 0,
1689 catalog_page: 0,
1690 catalog_checksum: 0,
1691 metadata_state_page: 0,
1692 metadata_state_checksum: 0,
1693 vector_artifact_page: 0,
1694 vector_artifact_checksum: 0,
1695 }
1696 }
1697}
1698
1699fn current_unix_ms() -> u64 {
1700 SystemTime::now()
1701 .duration_since(UNIX_EPOCH)
1702 .unwrap_or_default()
1703 .as_millis()
1704 .min(u128::from(u64::MAX)) as u64
1705}
1706
1707#[cfg(test)]
1708mod catalog_observation_tests {
1709 use super::*;
1710
1711 #[test]
1712 fn summary_free_legacy_native_state_rebuilds_a_missing_sidecar_after_catalog_publication() {
1713 let directory = tempfile::tempdir().expect("temporary database");
1714 let path = directory.path().join("legacy-summary-free.rdb");
1715 let options = RedDBOptions::persistent(&path)
1716 .with_memory_budget(64 * 1024 * 1024)
1717 .with_storage_profile(crate::StorageDeployPreset::PrimaryReplicaDev.selection())
1718 .expect("paged profile");
1719 let runtime = crate::RedDBRuntime::with_options(options).expect("paged runtime");
1720 runtime
1721 .execute_query("CREATE TABLE retained (value INTEGER)")
1722 .expect("retained table");
1723 runtime
1724 .execute_query("INSERT INTO retained (value) VALUES (17)")
1725 .expect("retained row");
1726 runtime.checkpoint().expect("publish retained row");
1727
1728 let db = runtime.db();
1729 let store = db.store();
1730 let mut header = store.physical_file_header().expect("physical header");
1731 assert_ne!(
1732 header.collection_roots_page, 0,
1733 "fixture retains native roots"
1734 );
1735 header.sequence = header
1736 .sequence
1737 .checked_add(1)
1738 .expect("next catalog sequence");
1739 header.manifest_page = 0;
1740 header.manifest_checksum = 0;
1741 header.registry_page = 0;
1742 header.registry_checksum = 0;
1743 header.recovery_page = 0;
1744 header.recovery_checksum = 0;
1745 header.catalog_page = 0;
1746 header.catalog_checksum = 0;
1747 header.metadata_state_page = 0;
1748 header.metadata_state_checksum = 0;
1749 header.vector_artifact_page = 0;
1750 header.vector_artifact_checksum = 0;
1751 store
1752 .update_physical_file_header(header)
1753 .expect("publish legacy catalog sequence");
1754
1755 let legacy = db.native_physical_state().expect("legacy native state");
1756 assert!(RedDB::native_state_is_summary_free_legacy(&legacy));
1757 let mut unreadable_publication = legacy.clone();
1758 unreadable_publication.header.registry_page = u32::MAX;
1759 assert!(
1760 !RedDB::native_state_is_summary_free_legacy(&unreadable_publication),
1761 "a non-zero unreadable summary pointer must still fail closed"
1762 );
1763
1764 let binary = PhysicalMetadataFile::metadata_binary_path_for(&path);
1765 let json = PhysicalMetadataFile::metadata_path_for(&path);
1766 db.delete_kv("red.storage.metadata", PHYSICAL_METADATA_KEY)
1767 .expect("legacy fixture");
1768 if binary.exists() {
1769 std::fs::remove_file(binary).expect("remove binary sidecar");
1770 }
1771 if json.exists() {
1772 std::fs::remove_file(json).expect("remove JSON sidecar");
1773 }
1774 for journal in PhysicalMetadataFile::journal_paths_for_data_path(&path)
1775 .expect("list metadata journals")
1776 {
1777 std::fs::remove_file(journal).expect("remove metadata journal");
1778 }
1779
1780 let metadata = db
1781 .load_or_bootstrap_physical_metadata(true)
1782 .expect("summary-free legacy state must bootstrap internal metadata");
1783 assert_eq!(
1784 metadata.superblock.collection_roots.get("retained"),
1785 db.physical_collection_roots().get("retained")
1786 );
1787 assert!(!PhysicalMetadataFile::metadata_binary_path_for(&path).exists());
1788 assert!(db.load_internal_physical_metadata().unwrap().is_some());
1789 let rows = runtime
1790 .execute_query("SELECT value FROM retained")
1791 .expect("retained row remains queryable");
1792 assert_eq!(rows.result.records.len(), 1);
1793 }
1794
1795 #[test]
1796 fn legacy_paged_drop_does_not_resurrect_derived_index_descriptors() {
1797 let directory = tempfile::tempdir().expect("temporary database");
1798 let path = directory.path().join("drop-descriptors.rdb");
1799 let options = RedDBOptions::persistent(&path)
1800 .with_memory_budget(64 * 1024 * 1024)
1801 .with_storage_profile(crate::StorageDeployPreset::PrimaryReplicaDev.selection())
1802 .expect("paged profile");
1803 {
1804 let runtime = crate::RedDBRuntime::with_options(options).expect("paged runtime");
1805 runtime
1806 .execute_query("CREATE TABLE retained_source (value INTEGER)")
1807 .expect("retained table");
1808 runtime
1809 .execute_query("INSERT INTO retained_source (value) VALUES (17)")
1810 .expect("retained row");
1811 runtime.checkpoint().expect("initial checkpoint");
1812 }
1813 let options = RedDBOptions::persistent(&path).with_memory_budget(64 * 1024 * 1024);
1816 {
1817 let runtime =
1818 crate::RedDBRuntime::with_options(options.clone()).expect("legacy reopen");
1819 assert!(runtime.db().is_paged());
1820 assert_eq!(
1821 runtime.db().options().storage_profile.deploy_profile,
1822 crate::storage::DeployProfile::Embedded
1823 );
1824 for cycle in 0..6 {
1826 let table = format!("discarded_source_{}", cycle % 4);
1827 runtime
1828 .execute_query(&format!("CREATE TABLE {table} (value INTEGER)"))
1829 .expect("temporary table");
1830 runtime.checkpoint().expect("publish derived descriptors");
1831 let before = runtime.db().physical_metadata().expect("physical metadata");
1832 assert!(before
1833 .indexes
1834 .iter()
1835 .any(|index| index.collection.as_deref() == Some(table.as_str())));
1836 runtime
1837 .execute_query(&format!("DROP TABLE {table}"))
1838 .expect("drop temporary table");
1839 for _ in 0..2 {
1840 runtime.checkpoint().expect("checkpoint after drop");
1841 let metadata = runtime.db().physical_metadata().expect("physical metadata");
1842 assert!(
1843 !metadata.indexes.iter().any(|index| index
1844 .collection
1845 .as_deref()
1846 .is_some_and(|name| name.starts_with("discarded_source_"))),
1847 "a dropped collection's derived descriptors were resurrected"
1848 );
1849 let native = runtime
1850 .db()
1851 .native_registry_summary()
1852 .expect("native registry");
1853 assert!(!native.indexes.iter().any(|index| {
1854 index
1855 .collection
1856 .as_deref()
1857 .is_some_and(|name| name.starts_with("discarded_source_"))
1858 }));
1859 }
1860 if cycle == 0 {
1861 let ghost = before
1864 .indexes
1865 .iter()
1866 .find(|index| index.collection.as_deref() == Some(table.as_str()))
1867 .expect("old derived descriptor")
1868 .clone();
1869 runtime
1870 .db()
1871 .update_physical_metadata(|metadata| {
1872 assert!(metadata.manifest_events.iter().any(|event| {
1873 event.collection == table
1874 && matches!(
1875 &event.kind,
1876 crate::physical::ManifestEventKind::Remove
1877 )
1878 }));
1879 metadata.indexes.push(ghost.clone());
1880 let mut orphan = ghost.clone();
1881 orphan.name = "audited_legacy_orphan".into();
1882 orphan.collection = Some("audited_missing_source".into());
1883 orphan.artifact_root_page = None;
1884 orphan.artifact_checksum = None;
1885 metadata.indexes.push(orphan.clone());
1886 orphan.name = "global_descriptor".into();
1887 orphan.collection = None;
1888 metadata.indexes.push(orphan);
1889 })
1890 .expect("simulate old metadata without deleting any rows");
1891 runtime
1892 .checkpoint()
1893 .expect("repair a confirmed old removal");
1894 }
1895 let metadata = runtime.db().physical_metadata().expect("physical metadata");
1896 assert!(metadata
1897 .indexes
1898 .iter()
1899 .any(|index| index.name == "audited_legacy_orphan"));
1900 assert!(metadata
1901 .indexes
1902 .iter()
1903 .any(|index| index.name == "global_descriptor"));
1904 }
1905 }
1906 let runtime = crate::RedDBRuntime::with_options(options).expect("second legacy reopen");
1907 assert_eq!(
1908 runtime
1909 .db()
1910 .store()
1911 .get_collection("retained_source")
1912 .expect("retained source")
1913 .count(),
1914 1
1915 );
1916 let metadata = runtime.db().physical_metadata().expect("physical metadata");
1917 assert!(!metadata.indexes.iter().any(|index| {
1918 index
1919 .collection
1920 .as_deref()
1921 .is_some_and(|name| name.starts_with("discarded_source_"))
1922 }));
1923 assert!(metadata
1924 .indexes
1925 .iter()
1926 .any(|index| index.collection.as_deref() == Some("retained_source")));
1927 assert!(metadata
1928 .indexes
1929 .iter()
1930 .any(|index| index.name == "audited_legacy_orphan"));
1931 assert!(metadata
1932 .indexes
1933 .iter()
1934 .any(|index| index.name == "global_descriptor"));
1935 }
1936
1937 #[test]
1938 fn index_reconciliation_reads_one_native_observation() {
1939 let directory = tempfile::tempdir().expect("temporary database");
1940 let runtime = crate::RedDBRuntime::with_options(
1941 RedDBOptions::persistent(directory.path().join("catalog.rdb"))
1942 .with_storage_profile(crate::StorageDeployPreset::Serverless.selection())
1943 .expect("paged profile")
1944 .with_capability(Capability::FullText),
1945 )
1946 .expect("runtime");
1947 for number in 0..4 {
1948 runtime
1949 .execute_query(&format!("CREATE TABLE source_{number} (payload TEXT)"))
1950 .expect("table");
1951 runtime
1952 .execute_query(&format!(
1953 "INSERT INTO source_{number} (payload) VALUES ('searchable payload')"
1954 ))
1955 .expect("row");
1956 }
1957 runtime.checkpoint().expect("published artifacts");
1958 let db = runtime.db();
1959 let pager = db.store.pager().expect("paged store");
1960 let reads = || {
1961 let stats = pager.cache_stats();
1962 stats.hits + stats.misses
1963 };
1964 let before = reads();
1965 let native = db.native_physical_state().expect("native state");
1966 let observation_reads = reads() - before;
1967 let artifacts = native.registry.expect("registry").vector_artifacts;
1968 let pages = native.vector_artifact_pages.expect("artifact pages");
1969 let indexes: Vec<_> = artifacts
1970 .iter()
1971 .filter(|artifact| artifact.artifact_kind == "text.fulltext")
1972 .map(|artifact| PhysicalIndexState {
1973 name: format!("{}::fulltext", artifact.collection),
1974 kind: IndexKind::FullText,
1975 collection: Some(artifact.collection.clone()),
1976 enabled: true,
1977 entries: 0,
1978 estimated_memory_bytes: 0,
1979 last_refresh_ms: None,
1980 backend: "catalog-derived".into(),
1981 artifact_kind: None,
1982 artifact_root_page: None,
1983 artifact_checksum: None,
1984 build_state: "catalog-derived".into(),
1985 })
1986 .collect();
1987 assert!(indexes.len() >= 4, "fixture must publish all four indexes");
1988 let declarations = indexes.clone();
1989 let before = reads();
1990 let indexes = db.reconcile_index_states_with_native_artifacts(indexes);
1991 let reconciliation_reads = reads() - before;
1992 for index in indexes {
1993 let artifact = artifacts
1994 .iter()
1995 .find(|artifact| {
1996 Some(&artifact.collection) == index.collection.as_ref()
1997 && artifact.artifact_kind == "text.fulltext"
1998 })
1999 .expect("published artifact");
2000 let page = pages
2001 .iter()
2002 .find(|page| {
2003 page.collection == artifact.collection
2004 && page.artifact_kind == artifact.artifact_kind
2005 })
2006 .expect("artifact root");
2007 assert_eq!(index.artifact_root_page, Some(page.root_page));
2008 assert_eq!(index.artifact_checksum, Some(artifact.checksum));
2009 assert_eq!(index.entries, artifact.vector_count as usize);
2010 assert_eq!(index.build_state, "artifact-published");
2011 }
2012 assert_eq!(
2013 reconciliation_reads, observation_reads,
2014 "catalog reconciliation must not reread artifact pages per index"
2015 );
2016
2017 let page_id = native.header.vector_artifact_page;
2020 let mut page = pager.read_page(page_id).expect("artifact directory");
2021 page.as_bytes_mut()[crate::storage::engine::HEADER_SIZE..].fill(0);
2022 pager
2023 .write_page(page_id, page)
2024 .expect("corrupt artifact directory");
2025 assert!(db.native_vector_artifact_pages().is_none());
2026 let indexes = db.reconcile_index_states_with_native_artifacts(declarations);
2027 for index in indexes {
2028 assert_eq!(index.artifact_root_page, None);
2029 assert!(
2030 index.artifact_checksum.is_some(),
2031 "registry remains available"
2032 );
2033 assert_eq!(index.build_state, "artifact-published");
2034 }
2035 }
2036}