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