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