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