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)> {
351 let manager = self.store.get_collection(collection)?;
352 let entities = manager.query_all(|_| true);
353 for entity in entities {
354 if let EntityData::Row(ref row) = entity.data {
355 if let Some(ref named) = row.named {
356 if let Some(Value::Text(ref k)) = named.get("key") {
357 if &**k == key {
358 let value = named.get("value").cloned().unwrap_or(Value::Null);
359 return Some((value, entity.id));
360 }
361 }
362 }
363 }
364 }
365 None
366 }
367
368 pub fn delete_kv(
370 &self,
371 collection: &str,
372 key: &str,
373 ) -> Result<bool, super::super::error::DevXError> {
374 let Some((_, id)) = self.get_kv(collection, key) else {
375 return Ok(false);
376 };
377 self.store
378 .delete(collection, id)
379 .map_err(|err| super::super::error::DevXError::Storage(format!("{err:?}")))?;
380 Ok(true)
381 }
382
383 pub(crate) fn with_initialized_metadata(self) -> Result<Self, Box<dyn std::error::Error>> {
384 if self.options.mode == StorageMode::Persistent {
385 match self.load_or_bootstrap_physical_metadata(false) {
387 Ok(metadata) => {
388 crate::reserved_fields::validate_physical_metadata_contracts(&metadata)
389 .map_err(|err| err.to_string())?;
390 }
391 Err(error)
392 if error
393 .downcast_ref::<std::io::Error>()
394 .is_some_and(crate::physical::is_collection_contract_decode_error) =>
395 {
396 return Err(error)
397 }
398 Err(_) => {}
399 }
400 }
404 self.load_collection_ttl_defaults_from_metadata();
405 if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
412 && self.options.storage_profile.packaging
413 == crate::storage::StoragePackaging::SingleFile
414 {
415 self.seed_contract_cache_from_store_aux()?;
416 }
417 self.load_hypertables_from_metadata();
421 Ok(self)
425 }
426
427 pub(crate) fn persist_metadata(&self) -> Result<(), Box<dyn std::error::Error>> {
428 if self.options.mode != StorageMode::Persistent || self.options.read_only {
429 return Ok(());
430 }
431 if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
432 && self.options.storage_profile.packaging
433 == crate::storage::StoragePackaging::SingleFile
434 {
435 return Ok(());
436 }
437 let Some(path) = self.path() else {
438 return Ok(());
439 };
440
441 let _publication = self.store.vault_publication_lock.lock();
442 let previous = self.load_or_bootstrap_physical_metadata(false).ok();
443 let collection_roots = self.physical_collection_roots();
444 let indexes = self
445 .native_physical_state()
446 .map(|state| self.physical_index_state_from_native_state(&state, previous.as_ref()))
447 .unwrap_or_else(|| self.physical_index_state());
448 let mut metadata = PhysicalMetadataFile::from_state(
449 self.options.clone(),
450 self.catalog_snapshot(),
451 collection_roots,
452 indexes,
453 previous.as_ref(),
454 );
455 metadata.collection_ttl_defaults_ms = self.collection_ttl_defaults_snapshot();
456 metadata.hypertables = self.hypertable_registry_snapshot();
460 metadata.save_for_data_path(path)?;
461 self.persist_native_physical_header(&metadata)?;
462 Ok(())
463 }
464
465 fn bootstrap_metadata_from_native_state(&self) -> Result<bool, Box<dyn std::error::Error>> {
466 if self.options.mode != StorageMode::Persistent || self.options.read_only {
467 return Ok(false);
468 }
469 let Some(path) = self.path() else {
470 return Ok(false);
471 };
472 let Some(native_state) = self.native_physical_state() else {
473 return Ok(false);
474 };
475 if !Self::native_state_is_bootstrap_complete(&native_state) {
476 return Ok(false);
477 }
478
479 let _publication = self.store.vault_publication_lock.lock();
480 let previous = PhysicalMetadataFile::load_for_data_path(path).ok();
481 let metadata = self.metadata_from_native_state(&native_state, previous.as_ref());
482 metadata.save_for_data_path(path)?;
483 self.persist_native_physical_header(&metadata)?;
484 Ok(true)
485 }
486
487 pub fn rebuild_physical_metadata_from_native_state(
490 &self,
491 ) -> Result<bool, Box<dyn std::error::Error>> {
492 self.bootstrap_metadata_from_native_state()
493 }
494
495 pub(crate) fn native_state_is_bootstrap_complete(native_state: &NativePhysicalState) -> bool {
496 let registry_complete = native_state.registry.as_ref().map(|registry| {
497 registry.collections_complete
498 && registry.indexes_complete
499 && registry.graph_projections_complete
500 && registry.analytics_jobs_complete
501 && registry.vector_artifacts_complete
502 });
503 let recovery_complete = native_state
504 .recovery
505 .as_ref()
506 .map(|recovery| recovery.snapshots_complete && recovery.exports_complete);
507 let catalog_complete = native_state
508 .catalog
509 .as_ref()
510 .map(|catalog| catalog.collections_complete);
511
512 registry_complete == Some(true)
513 && recovery_complete == Some(true)
514 && catalog_complete == Some(true)
515 }
516
517 pub(crate) fn load_or_bootstrap_physical_metadata(
518 &self,
519 persist_bootstrapped: bool,
520 ) -> Result<PhysicalMetadataFile, Box<dyn std::error::Error>> {
521 if self.options.mode != StorageMode::Persistent {
522 return Err("physical metadata requires persistent mode".into());
523 }
524 let Some(path) = self.path() else {
525 return Err("database path is not available".into());
526 };
527 let _publication = self.store.vault_publication_lock.lock();
528 let native_state = self.native_physical_state();
529
530 match PhysicalMetadataFile::load_for_data_path(path) {
531 Ok(metadata) => {
532 if let Some(native_state) = native_state.as_ref() {
533 let inspection = Self::inspect_native_header_against_metadata(
534 native_state.header,
535 &metadata,
536 );
537 if Self::repair_policy_for_inspection(&inspection)
538 == NativeHeaderRepairPolicy::NativeAheadOfMetadata
539 {
540 let bootstrapped =
541 self.metadata_from_native_state(native_state, Some(&metadata));
542 if persist_bootstrapped && !self.options.read_only {
543 bootstrapped.save_for_data_path(path)?;
544 self.persist_native_physical_header(&bootstrapped)?;
545 }
546 return Ok(bootstrapped);
547 }
548 }
549 Ok(metadata)
550 }
551 Err(err) => {
552 if crate::physical::is_collection_contract_decode_error(&err) {
553 return Err(err.into());
554 }
555 let Some(native_state) = native_state else {
556 return Err(err.into());
557 };
558 let is_fresh_empty = native_state.header.sequence == 0
574 && native_state.registry.is_none()
575 && native_state.catalog.is_none()
576 && native_state.recovery.is_none();
577 if !is_fresh_empty && !Self::native_state_is_bootstrap_complete(&native_state) {
578 return Err(err.into());
579 }
580 let metadata = self.metadata_from_native_state(&native_state, None);
581 if persist_bootstrapped && !self.options.read_only {
582 metadata.save_for_data_path(path)?;
583 self.persist_native_physical_header(&metadata)?;
584 }
585 Ok(metadata)
586 }
587 }
588 }
589
590 pub(crate) fn physical_metadata_preference(&self) -> Option<&'static str> {
591 let path = self.path()?;
592 let native_state = self.native_physical_state();
593 let metadata = PhysicalMetadataFile::load_for_data_path(path).ok();
594
595 match (metadata, native_state) {
596 (Some(metadata), Some(native_state)) => {
597 let inspection =
598 Self::inspect_native_header_against_metadata(native_state.header, &metadata);
599 match Self::repair_policy_for_inspection(&inspection) {
600 NativeHeaderRepairPolicy::InSync => Some("sidecar_current"),
601 NativeHeaderRepairPolicy::RepairNativeFromMetadata => Some("sidecar_current"),
602 NativeHeaderRepairPolicy::NativeAheadOfMetadata => Some("native_ahead"),
603 }
604 }
605 (Some(_), None) => Some("sidecar_only"),
606 (None, Some(_)) => Some("sidecar_missing_native_available"),
607 (None, None) => Some("sidecar_missing_no_native"),
608 }
609 }
610
611 fn metadata_from_native_state(
612 &self,
613 native_state: &NativePhysicalState,
614 previous: Option<&PhysicalMetadataFile>,
615 ) -> PhysicalMetadataFile {
616 let now = SystemTime::now()
617 .duration_since(UNIX_EPOCH)
618 .unwrap_or_default()
619 .as_millis();
620 let catalog = self.catalog_snapshot();
621 let catalog_name = catalog.name.clone();
622 let catalog_total_entities = catalog.total_entities;
623 let catalog_total_collections = catalog.total_collections;
624 let indexes = self.physical_index_state();
625
626 let mut manifest =
627 crate::api::SchemaManifest::now(self.options.clone(), catalog.total_collections);
628 manifest.updated_at_unix_ms = now;
629
630 let manifest_events = native_state
631 .manifest
632 .as_ref()
633 .map(|summary| {
634 summary
635 .recent_events
636 .iter()
637 .map(|event| crate::physical::ManifestEvent {
638 collection: event.collection.clone(),
639 object_key: event.object_key.clone(),
640 kind: match event.kind.as_str() {
641 "insert" => crate::physical::ManifestEventKind::Insert,
642 "update" => crate::physical::ManifestEventKind::Update,
643 "remove" => crate::physical::ManifestEventKind::Remove,
644 _ => crate::physical::ManifestEventKind::Checkpoint,
645 },
646 block: crate::physical::BlockReference {
647 index: event.block_index,
648 checksum: event.block_checksum,
649 },
650 snapshot_min: event.snapshot_min,
651 snapshot_max: event.snapshot_max,
652 })
653 .collect()
654 })
655 .unwrap_or_default();
656
657 let graph_projections = native_state
658 .registry
659 .as_ref()
660 .and_then(|registry| {
661 registry.graph_projections_complete.then(|| {
662 registry
663 .graph_projections
664 .iter()
665 .map(|projection| crate::physical::PhysicalGraphProjection {
666 name: projection.name.clone(),
667 created_at_unix_ms: projection.created_at_unix_ms,
668 updated_at_unix_ms: projection.updated_at_unix_ms,
669 state: "materialized".to_string(),
670 source: projection.source.clone(),
671 node_labels: projection.node_labels.clone(),
672 node_types: projection.node_types.clone(),
673 edge_labels: projection.edge_labels.clone(),
674 last_materialized_sequence: projection.last_materialized_sequence,
675 })
676 .collect()
677 })
678 })
679 .or_else(|| previous.map(|metadata| metadata.graph_projections.clone()))
680 .unwrap_or_default();
681
682 let analytics_jobs = native_state
683 .registry
684 .as_ref()
685 .and_then(|registry| {
686 registry.analytics_jobs_complete.then(|| {
687 registry
688 .analytics_jobs
689 .iter()
690 .map(|job| crate::physical::PhysicalAnalyticsJob {
691 id: job.id.clone(),
692 kind: job.kind.clone(),
693 state: job.state.clone(),
694 projection: job.projection.clone(),
695 created_at_unix_ms: job.created_at_unix_ms,
696 updated_at_unix_ms: job.updated_at_unix_ms,
697 last_run_sequence: job.last_run_sequence,
698 metadata: job.metadata.clone(),
699 })
700 .collect()
701 })
702 })
703 .or_else(|| previous.map(|metadata| metadata.analytics_jobs.clone()))
704 .unwrap_or_default();
705
706 let exports = native_state
707 .recovery
708 .as_ref()
709 .and_then(|recovery| {
710 recovery.exports_complete.then(|| {
711 recovery
712 .exports
713 .iter()
714 .map(|export| crate::physical::ExportDescriptor {
715 name: export.name.clone(),
716 created_at_unix_ms: export.created_at_unix_ms,
717 snapshot_id: export.snapshot_id,
718 superblock_sequence: export.superblock_sequence,
719 data_path: self
720 .path()
721 .map(|path| {
722 crate::physical::PhysicalMetadataFile::export_data_path_for(
723 path,
724 &export.name,
725 )
726 .display()
727 .to_string()
728 })
729 .unwrap_or_default(),
730 metadata_path: self
731 .path()
732 .map(|path| {
733 let export_data_path =
734 crate::physical::PhysicalMetadataFile::export_data_path_for(
735 path,
736 &export.name,
737 );
738 crate::physical::PhysicalMetadataFile::metadata_path_for(
739 &export_data_path,
740 )
741 .display()
742 .to_string()
743 })
744 .unwrap_or_default(),
745 collection_count: export.collection_count as usize,
746 total_entities: export.total_entities as usize,
747 })
748 .collect()
749 })
750 })
751 .or_else(|| previous.map(|metadata| metadata.exports.clone()))
752 .unwrap_or_default();
753
754 let snapshots = native_state
755 .recovery
756 .as_ref()
757 .and_then(|recovery| {
758 recovery.snapshots_complete.then(|| {
759 recovery
760 .snapshots
761 .iter()
762 .map(|snapshot| crate::physical::SnapshotDescriptor {
763 snapshot_id: snapshot.snapshot_id,
764 created_at_unix_ms: snapshot.created_at_unix_ms,
765 superblock_sequence: snapshot.superblock_sequence,
766 collection_count: snapshot.collection_count as usize,
767 total_entities: snapshot.total_entities as usize,
768 })
769 .collect()
770 })
771 })
772 .or_else(|| previous.map(|metadata| metadata.snapshots.clone()))
773 .unwrap_or_else(|| {
774 vec![crate::physical::SnapshotDescriptor {
775 snapshot_id: native_state.header.sequence,
776 created_at_unix_ms: now,
777 superblock_sequence: native_state.header.sequence,
778 collection_count: catalog_total_collections,
779 total_entities: catalog_total_entities,
780 }]
781 });
782
783 let catalog_stats = native_state
784 .catalog
785 .as_ref()
786 .and_then(|native_catalog| {
787 native_catalog.collections_complete.then(|| {
788 native_catalog
789 .collections
790 .iter()
791 .map(|collection| {
792 (
793 collection.name.clone(),
794 crate::api::CollectionStats {
795 entities: collection.entities as usize,
796 cross_refs: collection.cross_refs as usize,
797 segments: collection.segments as usize,
798 },
799 )
800 })
801 .collect::<BTreeMap<_, _>>()
802 })
803 })
804 .or_else(|| previous.map(|metadata| metadata.catalog.stats_by_collection.clone()))
805 .unwrap_or_else(|| catalog.stats_by_collection.clone());
806
807 PhysicalMetadataFile {
808 protocol_version: crate::physical::PHYSICAL_METADATA_PROTOCOL_VERSION.to_string(),
809 generated_at_unix_ms: now,
810 last_loaded_from: Some("native_bootstrap".to_string()),
811 last_healed_at_unix_ms: Some(now),
812 manifest,
813 catalog: crate::api::CatalogSnapshot {
814 name: catalog_name,
815 total_entities: native_state
816 .catalog
817 .as_ref()
818 .map(|summary| summary.total_entities as usize)
819 .unwrap_or(catalog_total_entities),
820 total_collections: native_state
821 .catalog
822 .as_ref()
823 .map(|summary| summary.collection_count as usize)
824 .unwrap_or(catalog_total_collections),
825 stats_by_collection: catalog_stats,
826 updated_at: SystemTime::now(),
827 },
828 manifest_events,
829 collection_ttl_defaults_ms: previous
830 .map(|metadata| metadata.collection_ttl_defaults_ms.clone())
831 .unwrap_or_default(),
832 collection_contracts: previous
833 .map(|metadata| metadata.collection_contracts.clone())
834 .unwrap_or_default(),
835 hypertables: previous
836 .map(|metadata| metadata.hypertables.clone())
837 .unwrap_or_default(),
838 tree_definitions: previous
839 .map(|metadata| metadata.tree_definitions.clone())
840 .unwrap_or_default(),
841 indexes,
842 graph_projections,
843 analytics_jobs,
844 exports,
845 superblock: crate::physical::SuperblockHeader {
846 format_version: native_state.header.format_version,
847 sequence: native_state.header.sequence,
848 copies: crate::physical::DEFAULT_SUPERBLOCK_COPIES,
849 manifest: crate::physical::ManifestPointers {
850 oldest: crate::physical::BlockReference {
851 index: native_state.header.manifest_oldest_root,
852 checksum: 0,
853 },
854 newest: crate::physical::BlockReference {
855 index: native_state.header.manifest_root,
856 checksum: 0,
857 },
858 },
859 free_set: crate::physical::BlockReference {
860 index: native_state.header.free_set_root,
861 checksum: 0,
862 },
863 collection_roots: native_state.collection_roots.clone(),
864 },
865 snapshots,
866 }
867 }
868
869 pub(crate) fn reconcile_index_states_with_native_artifacts(
870 &self,
871 mut indexes: Vec<PhysicalIndexState>,
872 ) -> Vec<PhysicalIndexState> {
873 let native_artifacts = self
874 .native_physical_state()
875 .and_then(|state| state.registry)
876 .map(|registry| registry.vector_artifacts)
877 .unwrap_or_default();
878 for index in &mut indexes {
879 let Some(collection) = index.collection.as_deref() else {
880 continue;
881 };
882 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
883 continue;
884 };
885 let Some(artifact) = native_artifacts.iter().find(|artifact| {
886 artifact.collection == collection && artifact.artifact_kind == artifact_kind
887 }) else {
888 index.build_state = "metadata-only".to_string();
889 continue;
890 };
891 index.entries = artifact.vector_count as usize;
892 index.estimated_memory_bytes = artifact.serialized_bytes;
893 index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
894 index.artifact_kind = Some(artifact.artifact_kind.clone());
895 index.artifact_checksum = Some(artifact.checksum);
896 index.build_state = "artifact-published".to_string();
897 if let Some(pages) = self.native_vector_artifact_pages() {
898 index.artifact_root_page = pages
899 .into_iter()
900 .find(|page| {
901 page.collection == artifact.collection
902 && page.artifact_kind == artifact.artifact_kind
903 })
904 .map(|page| page.root_page);
905 }
906 }
907 indexes
908 }
909
910 pub(crate) fn warmup_native_vector_artifact_for_index(
911 &self,
912 index: &PhysicalIndexState,
913 ) -> Result<(), String> {
914 let Some(collection) = index.collection.as_deref() else {
915 return Ok(());
916 };
917 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
918 return Ok(());
919 };
920 self.warmup_native_vector_artifact(collection, Some(artifact_kind))?;
921 Ok(())
922 }
923
924 pub(crate) fn apply_runtime_native_artifact_to_index_state(
925 &self,
926 index: &mut PhysicalIndexState,
927 ) -> Result<(), String> {
928 let Some(collection) = index.collection.as_deref() else {
929 return Ok(());
930 };
931 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
932 return Ok(());
933 };
934 let artifact = self.inspect_native_vector_artifact(collection, Some(artifact_kind))?;
935 index.entries = artifact
936 .graph_edge_count
937 .or(artifact.text_posting_count)
938 .unwrap_or(artifact.node_count) as usize;
939 index.estimated_memory_bytes = artifact.byte_len;
940 index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
941 index.artifact_kind = Some(artifact.artifact_kind.clone());
942 index.artifact_checksum = Some(artifact.checksum);
943 index.build_state = "ready".to_string();
944 index.artifact_root_page = self
945 .native_vector_artifact_pages()
946 .and_then(|pages| {
947 pages.into_iter().find(|page| {
948 page.collection == artifact.collection
949 && page.artifact_kind == artifact.artifact_kind
950 })
951 })
952 .map(|page| page.root_page);
953 Ok(())
954 }
955
956 pub(crate) fn physical_index_state_from_native_state(
957 &self,
958 native_state: &NativePhysicalState,
959 previous: Option<&PhysicalMetadataFile>,
960 ) -> Vec<PhysicalIndexState> {
961 let mut fresh = self.physical_index_state();
962 let Some(registry) = native_state.registry.as_ref() else {
963 if let Some(previous) = previous {
964 for index in &previous.indexes {
965 if !fresh.iter().any(|candidate| candidate.name == index.name) {
966 fresh.push(index.clone());
967 }
968 }
969 }
970 return fresh;
971 };
972
973 for index in &mut fresh {
974 if let Some(native) = registry
975 .indexes
976 .iter()
977 .find(|candidate| candidate.name == index.name)
978 {
979 index.enabled = native.enabled;
980 index.last_refresh_ms = native.last_refresh_ms;
981 index.backend = native.backend.clone();
982 index.entries = native.entries as usize;
983 index.estimated_memory_bytes = native.estimated_memory_bytes;
984 if index.artifact_kind.is_none() {
985 index.artifact_kind = Self::native_artifact_kind_for_index(index.kind)
986 .map(|value| value.to_string());
987 }
988 if index.build_state == "catalog-derived" {
989 index.build_state = "registry-loaded".to_string();
990 }
991 }
992 }
993
994 for native in ®istry.indexes {
995 if fresh.iter().any(|index| index.name == native.name) {
996 continue;
997 }
998 let Some(kind) = Self::index_kind_from_str(&native.kind) else {
999 continue;
1000 };
1001 fresh.push(PhysicalIndexState {
1002 name: native.name.clone(),
1003 kind,
1004 collection: native.collection.clone(),
1005 enabled: native.enabled,
1006 entries: native.entries as usize,
1007 estimated_memory_bytes: native.estimated_memory_bytes,
1008 last_refresh_ms: native.last_refresh_ms,
1009 backend: native.backend.clone(),
1010 artifact_kind: Self::native_artifact_kind_for_index(kind)
1011 .map(|value| value.to_string()),
1012 artifact_root_page: None,
1013 artifact_checksum: None,
1014 build_state: "registry-loaded".to_string(),
1015 });
1016 }
1017
1018 if !registry.indexes_complete {
1019 if let Some(previous) = previous {
1020 for index in &previous.indexes {
1021 if !fresh.iter().any(|candidate| candidate.name == index.name) {
1022 fresh.push(index.clone());
1023 }
1024 }
1025 }
1026 }
1027
1028 fresh
1029 }
1030
1031 pub(crate) fn graph_projections_from_native_state(
1032 &self,
1033 native_state: &NativePhysicalState,
1034 ) -> Vec<PhysicalGraphProjection> {
1035 native_state
1036 .registry
1037 .as_ref()
1038 .map(|registry| {
1039 registry
1040 .graph_projections
1041 .iter()
1042 .map(|projection| PhysicalGraphProjection {
1043 name: projection.name.clone(),
1044 created_at_unix_ms: projection.created_at_unix_ms,
1045 updated_at_unix_ms: projection.updated_at_unix_ms,
1046 state: "materialized".to_string(),
1047 source: projection.source.clone(),
1048 node_labels: projection.node_labels.clone(),
1049 node_types: projection.node_types.clone(),
1050 edge_labels: projection.edge_labels.clone(),
1051 last_materialized_sequence: projection.last_materialized_sequence,
1052 })
1053 .collect()
1054 })
1055 .unwrap_or_default()
1056 }
1057
1058 pub(crate) fn analytics_jobs_from_native_state(
1059 &self,
1060 native_state: &NativePhysicalState,
1061 ) -> Vec<PhysicalAnalyticsJob> {
1062 native_state
1063 .registry
1064 .as_ref()
1065 .map(|registry| {
1066 registry
1067 .analytics_jobs
1068 .iter()
1069 .map(|job| PhysicalAnalyticsJob {
1070 id: job.id.clone(),
1071 kind: job.kind.clone(),
1072 state: job.state.clone(),
1073 projection: job.projection.clone(),
1074 created_at_unix_ms: job.created_at_unix_ms,
1075 updated_at_unix_ms: job.updated_at_unix_ms,
1076 last_run_sequence: job.last_run_sequence,
1077 metadata: job.metadata.clone(),
1078 })
1079 .collect()
1080 })
1081 .unwrap_or_default()
1082 }
1083
1084 pub(crate) fn exports_from_native_state(
1085 &self,
1086 native_state: &NativePhysicalState,
1087 ) -> Vec<ExportDescriptor> {
1088 native_state
1089 .recovery
1090 .as_ref()
1091 .map(|recovery| {
1092 recovery
1093 .exports
1094 .iter()
1095 .map(|export| ExportDescriptor {
1096 name: export.name.clone(),
1097 created_at_unix_ms: export.created_at_unix_ms,
1098 snapshot_id: export.snapshot_id,
1099 superblock_sequence: export.superblock_sequence,
1100 data_path: self
1101 .path()
1102 .map(|path| {
1103 crate::physical::PhysicalMetadataFile::export_data_path_for(
1104 path,
1105 &export.name,
1106 )
1107 .display()
1108 .to_string()
1109 })
1110 .unwrap_or_default(),
1111 metadata_path: self
1112 .path()
1113 .map(|path| {
1114 let export_data_path =
1115 crate::physical::PhysicalMetadataFile::export_data_path_for(
1116 path,
1117 &export.name,
1118 );
1119 crate::physical::PhysicalMetadataFile::metadata_path_for(
1120 &export_data_path,
1121 )
1122 .display()
1123 .to_string()
1124 })
1125 .unwrap_or_default(),
1126 collection_count: export.collection_count as usize,
1127 total_entities: export.total_entities as usize,
1128 })
1129 .collect()
1130 })
1131 .unwrap_or_default()
1132 }
1133
1134 pub(crate) fn snapshots_from_native_state(
1135 &self,
1136 native_state: &NativePhysicalState,
1137 ) -> Vec<crate::physical::SnapshotDescriptor> {
1138 let snapshots: Vec<_> = native_state
1139 .recovery
1140 .as_ref()
1141 .map(|recovery| {
1142 recovery
1143 .snapshots
1144 .iter()
1145 .map(|snapshot| crate::physical::SnapshotDescriptor {
1146 snapshot_id: snapshot.snapshot_id,
1147 created_at_unix_ms: snapshot.created_at_unix_ms,
1148 superblock_sequence: snapshot.superblock_sequence,
1149 collection_count: snapshot.collection_count as usize,
1150 total_entities: snapshot.total_entities as usize,
1151 })
1152 .collect()
1153 })
1154 .unwrap_or_default();
1155 if !snapshots.is_empty() {
1156 return snapshots;
1157 }
1158
1159 let now = SystemTime::now()
1160 .duration_since(UNIX_EPOCH)
1161 .unwrap_or_default()
1162 .as_millis();
1163 let (collection_count, total_entities) = native_state
1164 .catalog
1165 .as_ref()
1166 .map(|catalog| {
1167 (
1168 catalog.collection_count as usize,
1169 catalog.total_entities as usize,
1170 )
1171 })
1172 .unwrap_or_else(|| {
1173 let catalog = self.catalog_snapshot();
1174 (catalog.total_collections, catalog.total_entities)
1175 });
1176
1177 vec![crate::physical::SnapshotDescriptor {
1178 snapshot_id: native_state.header.sequence,
1179 created_at_unix_ms: now,
1180 superblock_sequence: native_state.header.sequence,
1181 collection_count,
1182 total_entities,
1183 }]
1184 }
1185
1186 fn index_kind_from_str(value: &str) -> Option<crate::index::IndexKind> {
1187 match value {
1188 "btree" => Some(crate::index::IndexKind::BTree),
1189 "vector.hnsw" => Some(crate::index::IndexKind::VectorHnsw),
1190 "vector.inverted" => Some(crate::index::IndexKind::VectorInverted),
1191 "vector.turbo" => Some(crate::index::IndexKind::VectorTurbo),
1192 "graph.adjacency" => Some(crate::index::IndexKind::GraphAdjacency),
1193 "text.fulltext" => Some(crate::index::IndexKind::FullText),
1194 "document.pathvalue" => Some(crate::index::IndexKind::DocumentPathValue),
1195 "search.hybrid" => Some(crate::index::IndexKind::HybridSearch),
1196 _ => None,
1197 }
1198 }
1199
1200 pub(crate) fn native_artifact_kind_for_index(kind: IndexKind) -> Option<&'static str> {
1201 match kind {
1202 IndexKind::VectorHnsw => Some("hnsw"),
1203 IndexKind::VectorInverted => Some("ivf"),
1204 IndexKind::VectorTurbo => Some("turboquant"),
1205 IndexKind::GraphAdjacency => Some("graph.adjacency"),
1206 IndexKind::FullText => Some("text.fulltext"),
1207 IndexKind::DocumentPathValue => Some("document.pathvalue"),
1208 _ => None,
1209 }
1210 }
1211
1212 fn index_is_declared(&self, name: &str) -> bool {
1213 self.physical_metadata()
1214 .map(|metadata| metadata.indexes.iter().any(|index| index.name == name))
1215 .unwrap_or(false)
1216 }
1217
1218 pub(crate) fn graph_projection_is_declared(&self, name: &str) -> bool {
1219 self.physical_metadata()
1220 .map(|metadata| {
1221 metadata
1222 .graph_projections
1223 .iter()
1224 .any(|projection| projection.name == name)
1225 })
1226 .unwrap_or(false)
1227 }
1228
1229 pub(crate) fn graph_projection_is_operational(&self, name: &str) -> bool {
1230 self.operational_graph_projections()
1231 .into_iter()
1232 .any(|projection| projection.name == name && projection.state == "materialized")
1233 }
1234
1235 pub(crate) fn analytics_job_id(kind: &str, projection: Option<&str>) -> String {
1236 match projection {
1237 Some(projection) => format!("{kind}::{projection}"),
1238 None => format!("{kind}::global"),
1239 }
1240 }
1241
1242 pub(crate) fn update_physical_metadata<T, F>(
1243 &self,
1244 mutator: F,
1245 ) -> Result<T, Box<dyn std::error::Error>>
1246 where
1247 F: FnOnce(&mut PhysicalMetadataFile) -> T,
1248 {
1249 if self.options.mode != StorageMode::Persistent {
1250 return Err("physical metadata operations require persistent mode".into());
1251 }
1252 if self.options.read_only {
1253 return Err("physical metadata operations are not allowed in read-only mode".into());
1254 }
1255 let Some(path) = self.path() else {
1256 return Err("database path is not available".into());
1257 };
1258
1259 let _publication = self.store.vault_publication_lock.lock();
1266 let mut metadata = self.load_or_bootstrap_physical_metadata(true)?;
1267
1268 if metadata.indexes.is_empty() {
1269 metadata.indexes = self.physical_index_state();
1270 }
1271 metadata.superblock.collection_roots = self.physical_collection_roots();
1272
1273 let result = mutator(&mut metadata);
1274 metadata.save_for_data_path(path)?;
1275 self.persist_native_physical_header(&metadata)?;
1276 Ok(result)
1277 }
1278
1279 pub(crate) fn persist_native_physical_header(
1280 &self,
1281 metadata: &PhysicalMetadataFile,
1282 ) -> Result<(), Box<dyn std::error::Error>> {
1283 if !self.paged_mode {
1284 return Ok(());
1285 }
1286
1287 let _publication = self.store.vault_publication_lock.lock();
1288 let existing_page = self
1289 .store
1290 .physical_file_header()
1291 .map(|header| header.collection_roots_page)
1292 .filter(|page| *page != 0);
1293 let existing_registry_page = self
1294 .store
1295 .physical_file_header()
1296 .map(|header| header.registry_page)
1297 .filter(|page| *page != 0);
1298 let existing_recovery_page = self
1299 .store
1300 .physical_file_header()
1301 .map(|header| header.recovery_page)
1302 .filter(|page| *page != 0);
1303 let existing_catalog_page = self
1304 .store
1305 .physical_file_header()
1306 .map(|header| header.catalog_page)
1307 .filter(|page| *page != 0);
1308 let existing_metadata_state_page = self
1309 .store
1310 .physical_file_header()
1311 .map(|header| header.metadata_state_page)
1312 .filter(|page| *page != 0);
1313 let existing_vector_artifact_page = self
1314 .store
1315 .physical_file_header()
1316 .map(|header| header.vector_artifact_page)
1317 .filter(|page| *page != 0);
1318 let existing_manifest_page = self
1319 .store
1320 .physical_file_header()
1321 .map(|header| header.manifest_page)
1322 .filter(|page| *page != 0);
1323 let (manifest_page, manifest_checksum) = self.store.write_native_manifest_summary(
1324 metadata.superblock.sequence,
1325 &metadata.manifest_events,
1326 existing_manifest_page,
1327 )?;
1328 let (collection_roots_page, collection_roots_checksum) = self
1329 .store
1330 .write_native_collection_roots(&metadata.superblock.collection_roots, existing_page)?;
1331 let vector_artifact_records = self.native_vector_artifact_records();
1334 let artifact_summaries = vector_artifact_records
1335 .iter()
1336 .map(|(summary, _)| summary.clone())
1337 .collect::<Vec<_>>();
1338 let registry_summary = self.native_registry_summary_from_metadata(
1339 metadata,
1340 &artifact_summaries,
1341 artifact_summaries.len(),
1342 );
1343 let (registry_page, registry_checksum) = self
1344 .store
1345 .write_native_registry_summary(®istry_summary, existing_registry_page)?;
1346 let recovery_summary = Self::native_recovery_summary_from_metadata(metadata);
1347 let (recovery_page, recovery_checksum) = self
1348 .store
1349 .write_native_recovery_summary(&recovery_summary, existing_recovery_page)?;
1350 let catalog_summary = Self::native_catalog_summary_from_metadata(metadata);
1351 let (catalog_page, catalog_checksum) = self
1352 .store
1353 .write_native_catalog_summary(&catalog_summary, existing_catalog_page)?;
1354 let metadata_state_summary = Self::native_metadata_state_summary_from_metadata(metadata);
1355 let (metadata_state_page, metadata_state_checksum) =
1356 self.store.write_native_metadata_state_summary(
1357 &metadata_state_summary,
1358 existing_metadata_state_page,
1359 )?;
1360 let vector_artifact_payloads = vector_artifact_records
1361 .iter()
1362 .map(|(summary, bytes)| {
1363 (
1364 summary.collection.clone(),
1365 summary.artifact_kind.clone(),
1366 bytes.clone(),
1367 )
1368 })
1369 .collect::<Vec<_>>();
1370 let (vector_artifact_page, vector_artifact_checksum, _vector_artifact_pages) =
1371 self.store.write_native_vector_artifact_store(
1372 &vector_artifact_payloads,
1373 existing_vector_artifact_page,
1374 )?;
1375 let mut header = Self::native_header_from_metadata(metadata);
1376 header.manifest_page = manifest_page;
1377 header.manifest_checksum = manifest_checksum;
1378 header.collection_roots_page = collection_roots_page;
1379 header.collection_roots_checksum = collection_roots_checksum;
1380 header.registry_page = registry_page;
1381 header.registry_checksum = registry_checksum;
1382 header.recovery_page = recovery_page;
1383 header.recovery_checksum = recovery_checksum;
1384 header.catalog_page = catalog_page;
1385 header.catalog_checksum = catalog_checksum;
1386 header.metadata_state_page = metadata_state_page;
1387 header.metadata_state_checksum = metadata_state_checksum;
1388 header.vector_artifact_page = vector_artifact_page;
1389 header.vector_artifact_checksum = vector_artifact_checksum;
1390 self.store.update_physical_file_header(header)?;
1391 self.store.persist()?;
1392 Ok(())
1393 }
1394
1395 pub(crate) fn native_header_from_metadata(
1396 metadata: &PhysicalMetadataFile,
1397 ) -> PhysicalFileHeader {
1398 PhysicalFileHeader {
1399 format_version: metadata.superblock.format_version,
1400 sequence: metadata.superblock.sequence,
1401 manifest_oldest_root: metadata.superblock.manifest.oldest.index,
1402 manifest_root: metadata.superblock.manifest.newest.index,
1403 free_set_root: metadata.superblock.free_set.index,
1404 manifest_page: 0,
1405 manifest_checksum: 0,
1406 collection_roots_page: 0,
1407 collection_roots_checksum: 0,
1408 collection_root_count: metadata.superblock.collection_roots.len() as u32,
1409 snapshot_count: metadata.snapshots.len() as u32,
1410 index_count: metadata.indexes.len() as u32,
1411 catalog_collection_count: metadata.catalog.total_collections as u32,
1412 catalog_total_entities: metadata.catalog.total_entities as u64,
1413 export_count: metadata.exports.len() as u32,
1414 graph_projection_count: metadata.graph_projections.len() as u32,
1415 analytics_job_count: metadata.analytics_jobs.len() as u32,
1416 manifest_event_count: metadata.manifest_events.len() as u32,
1417 registry_page: 0,
1418 registry_checksum: 0,
1419 recovery_page: 0,
1420 recovery_checksum: 0,
1421 catalog_page: 0,
1422 catalog_checksum: 0,
1423 metadata_state_page: 0,
1424 metadata_state_checksum: 0,
1425 vector_artifact_page: 0,
1426 vector_artifact_checksum: 0,
1427 }
1428 }
1429}
1430
1431fn current_unix_ms() -> u64 {
1432 SystemTime::now()
1433 .duration_since(UNIX_EPOCH)
1434 .unwrap_or_default()
1435 .as_millis()
1436 .min(u128::from(u64::MAX)) as u64
1437}