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