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