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 Ok(mut metadata) = self.load_or_bootstrap_physical_metadata(true) else {
25 return Ok(());
26 };
27
28 self.prune_export_registry(&mut metadata.exports);
29 metadata.save_for_data_path(path)?;
30 }
31
32 let _ = self.sweep_ttl_expired_entities()?;
33
34 Ok(())
35 }
36
37 pub(crate) fn ttl_expired_entities_now(
38 &self,
39 ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
40 self.ttl_expired_entities_at(current_unix_ms())
41 }
42
43 pub fn replica_allows_entity_at_read(
44 &self,
45 collection: &str,
46 entity: &crate::storage::UnifiedEntity,
47 ) -> bool {
48 if !self.is_replica_role() {
49 return true;
50 }
51 !self.entity_expired_at(collection, entity, current_unix_ms())
52 }
53
54 fn sweep_ttl_expired_entities(&self) -> Result<usize, Box<dyn std::error::Error>> {
55 let to_delete = self.ttl_expired_entities_now()?;
56
57 let mut deleted = 0usize;
58 for (collection, id) in to_delete {
59 match self.store.delete(&collection, id) {
60 Ok(true) => deleted = deleted.saturating_add(1),
61 Ok(false) => {}
62 Err(err) => {
63 return Err(format!(
64 "failed deleting expired entity {id} from collection '{collection}': {err:?}"
65 )
66 .into());
67 }
68 }
69 }
70
71 Ok(deleted)
72 }
73
74 fn ttl_expired_entities_at(
75 &self,
76 now_ms: u64,
77 ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
78 let mut to_delete = Vec::<(String, EntityId)>::new();
79
80 let mut absolute_expired = self.expired_entities_by_expires_at(now_ms)?;
81 to_delete.append(&mut absolute_expired);
82
83 let mut relative_expired = self.expired_entities_by_ttl(now_ms)?;
84 to_delete.append(&mut relative_expired);
85
86 to_delete.sort_unstable();
87 to_delete.dedup();
88
89 Ok(to_delete)
90 }
91
92 fn entity_expired_at(
93 &self,
94 collection: &str,
95 entity: &crate::storage::UnifiedEntity,
96 now_ms: u64,
97 ) -> bool {
98 let Some(metadata) = self.store.get_metadata(collection, entity.id) else {
99 return false;
100 };
101
102 if metadata
103 .get("_expires_at")
104 .and_then(Self::metadata_u64)
105 .is_some_and(|expires_at_ms| expires_at_ms <= now_ms)
106 {
107 return true;
108 }
109
110 let ttl_ms = metadata.get("_ttl_ms").and_then(Self::metadata_u64);
111 let ttl_secs = if ttl_ms.is_none() {
112 metadata.get("_ttl").and_then(|value| {
113 Self::metadata_u64(value).and_then(|value_secs| value_secs.checked_mul(1000))
114 })
115 } else {
116 None
117 };
118
119 let Some(ttl_ms) = ttl_ms.or(ttl_secs) else {
120 return false;
121 };
122 entity
123 .created_at
124 .saturating_mul(1000)
125 .saturating_add(ttl_ms)
126 <= now_ms
127 }
128
129 fn expired_entities_by_expires_at(
130 &self,
131 now_ms: u64,
132 ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
133 let mut ids = self.store.filter_metadata_all(&[(
134 "_expires_at".to_string(),
135 MetadataFilter::Le(MetadataValue::Timestamp(now_ms)),
136 )]);
137
138 if let Ok(now_ms_i64) = i64::try_from(now_ms) {
139 ids.extend(self.store.filter_metadata_all(&[(
140 "_expires_at".to_string(),
141 MetadataFilter::Le(MetadataValue::Int(now_ms_i64)),
142 )]));
143 }
144
145 let now_ms_f64 = now_ms as f64;
146 if now_ms_f64.is_finite() {
147 ids.extend(self.store.filter_metadata_all(&[(
148 "_expires_at".to_string(),
149 MetadataFilter::Le(MetadataValue::Float(now_ms_f64)),
150 )]));
151 }
152
153 Ok(ids)
154 }
155
156 fn expired_entities_by_ttl(
157 &self,
158 now_ms: u64,
159 ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
160 let mut candidates = Vec::<(String, EntityId)>::new();
161
162 let ttl_ms_candidates = self
163 .store
164 .filter_metadata_all(&[("_ttl_ms".to_string(), MetadataFilter::IsNotNull)]);
165 candidates.extend(ttl_ms_candidates);
166
167 let ttl_candidates = self
168 .store
169 .filter_metadata_all(&[("_ttl".to_string(), MetadataFilter::IsNotNull)]);
170 candidates.extend(ttl_candidates);
171
172 if candidates.is_empty() {
173 return Ok(Vec::new());
174 }
175
176 candidates.sort_unstable();
177 candidates.dedup();
178
179 let mut expired = Vec::<(String, EntityId)>::new();
180 for (collection, entity_id) in candidates {
181 let Some(entity) = self.store.get(&collection, entity_id) else {
182 continue;
183 };
184
185 let Some(metadata) = self.store.get_metadata(&collection, entity_id) else {
186 continue;
187 };
188
189 let ttl_ms = metadata.get("_ttl_ms").and_then(Self::metadata_u64);
190 let ttl_secs = if ttl_ms.is_none() {
191 metadata.get("_ttl").and_then(|value| {
192 Self::metadata_u64(value).and_then(|value_secs| value_secs.checked_mul(1000))
193 })
194 } else {
195 None
196 };
197
198 let Some(ttl_ms) = ttl_ms.or(ttl_secs) else {
199 continue;
200 };
201
202 let created_at_ms = entity.created_at.saturating_mul(1000);
203 let expiry_ms = created_at_ms.saturating_add(ttl_ms);
204 if expiry_ms <= now_ms {
205 expired.push((collection, entity_id));
206 }
207 }
208
209 Ok(expired)
210 }
211
212 fn metadata_u64(value: &MetadataValue) -> Option<u64> {
213 match value {
214 MetadataValue::Int(v) if *v >= 0 => Some(*v as u64),
215 MetadataValue::Timestamp(v) => Some(*v),
216 MetadataValue::Float(v) => {
217 if !v.is_finite() || !v.is_sign_positive() || v.fract().abs() >= f64::EPSILON {
218 return None;
219 }
220 if *v > u64::MAX as f64 {
221 return None;
222 }
223 Some(v.trunc() as u64)
224 }
225 MetadataValue::String(v) => v.parse::<u64>().ok(),
226 _ => None,
227 }
228 }
229
230 pub fn node(&self, collection: impl Into<String>, label: impl Into<String>) -> NodeBuilder {
243 NodeBuilder::new(
244 self.store.clone(),
245 self.preprocessors.clone(),
246 collection,
247 label,
248 )
249 }
250
251 pub fn edge(&self, collection: impl Into<String>, label: impl Into<String>) -> EdgeBuilder {
263 EdgeBuilder::new(
264 self.store.clone(),
265 self.preprocessors.clone(),
266 collection,
267 label,
268 )
269 }
270
271 pub fn vector(&self, collection: impl Into<String>) -> VectorBuilder {
282 VectorBuilder::new(self.store.clone(), self.preprocessors.clone(), collection)
283 }
284
285 pub fn row(&self, table: impl Into<String>, columns: Vec<(&str, Value)>) -> RowBuilder {
296 RowBuilder::new(
297 self.store.clone(),
298 self.preprocessors.clone(),
299 table,
300 columns,
301 )
302 }
303
304 pub fn doc(&self, collection: impl Into<String>) -> DocumentBuilder {
318 DocumentBuilder::new(self.store.clone(), self.preprocessors.clone(), collection)
319 }
320
321 pub fn kv(
332 &self,
333 collection: impl Into<String>,
334 key: impl Into<String>,
335 value: Value,
336 ) -> KvBuilder {
337 KvBuilder::new(
338 self.store.clone(),
339 self.preprocessors.clone(),
340 collection,
341 key,
342 value,
343 )
344 }
345
346 pub fn get_kv(&self, collection: &str, key: &str) -> Option<(Value, EntityId)> {
350 let manager = self.store.get_collection(collection)?;
351 let entities = manager.query_all(|_| true);
352 for entity in entities {
353 if let EntityData::Row(ref row) = entity.data {
354 if let Some(ref named) = row.named {
355 if let Some(Value::Text(ref k)) = named.get("key") {
356 if &**k == key {
357 let value = named.get("value").cloned().unwrap_or(Value::Null);
358 return Some((value, entity.id));
359 }
360 }
361 }
362 }
363 }
364 None
365 }
366
367 pub fn delete_kv(
369 &self,
370 collection: &str,
371 key: &str,
372 ) -> Result<bool, super::super::error::DevXError> {
373 let Some((_, id)) = self.get_kv(collection, key) else {
374 return Ok(false);
375 };
376 self.store
377 .delete(collection, id)
378 .map_err(|err| super::super::error::DevXError::Storage(format!("{err:?}")))?;
379 Ok(true)
380 }
381
382 pub(crate) fn with_initialized_metadata(self) -> Result<Self, Box<dyn std::error::Error>> {
383 if self.options.mode == StorageMode::Persistent {
384 match self.load_or_bootstrap_physical_metadata(false) {
386 Ok(metadata) => {
387 crate::reserved_fields::validate_physical_metadata_contracts(&metadata)
388 .map_err(|err| err.to_string())?;
389 }
390 Err(error)
391 if error
392 .downcast_ref::<std::io::Error>()
393 .is_some_and(crate::physical::is_collection_contract_decode_error) =>
394 {
395 return Err(error)
396 }
397 Err(_) => {}
398 }
399 }
403 self.load_collection_ttl_defaults_from_metadata();
404 if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
411 && self.options.storage_profile.packaging
412 == crate::storage::StoragePackaging::SingleFile
413 {
414 self.seed_contract_cache_from_store_aux()?;
415 }
416 self.load_hypertables_from_metadata();
420 self.recover_queue_pending_state();
421 Ok(self)
422 }
423
424 pub(crate) fn persist_metadata(&self) -> Result<(), Box<dyn std::error::Error>> {
425 if self.options.mode != StorageMode::Persistent || self.options.read_only {
426 return Ok(());
427 }
428 if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
429 && self.options.storage_profile.packaging
430 == crate::storage::StoragePackaging::SingleFile
431 {
432 return Ok(());
433 }
434 let Some(path) = self.path() else {
435 return Ok(());
436 };
437
438 let previous = self.load_or_bootstrap_physical_metadata(false).ok();
439 let collection_roots = self.physical_collection_roots();
440 let indexes = self
441 .native_physical_state()
442 .map(|state| self.physical_index_state_from_native_state(&state, previous.as_ref()))
443 .unwrap_or_else(|| self.physical_index_state());
444 let mut metadata = PhysicalMetadataFile::from_state(
445 self.options.clone(),
446 self.catalog_snapshot(),
447 collection_roots,
448 indexes,
449 previous.as_ref(),
450 );
451 metadata.collection_ttl_defaults_ms = self.collection_ttl_defaults_snapshot();
452 metadata.hypertables = self.hypertable_registry_snapshot();
456 metadata.save_for_data_path(path)?;
457 self.persist_native_physical_header(&metadata)?;
458 Ok(())
459 }
460
461 fn bootstrap_metadata_from_native_state(&self) -> Result<bool, Box<dyn std::error::Error>> {
462 if self.options.mode != StorageMode::Persistent || self.options.read_only {
463 return Ok(false);
464 }
465 let Some(path) = self.path() else {
466 return Ok(false);
467 };
468 let Some(native_state) = self.native_physical_state() else {
469 return Ok(false);
470 };
471 if !Self::native_state_is_bootstrap_complete(&native_state) {
472 return Ok(false);
473 }
474
475 let previous = PhysicalMetadataFile::load_for_data_path(path).ok();
476 let metadata = self.metadata_from_native_state(&native_state, previous.as_ref());
477 metadata.save_for_data_path(path)?;
478 self.persist_native_physical_header(&metadata)?;
479 Ok(true)
480 }
481
482 pub fn rebuild_physical_metadata_from_native_state(
485 &self,
486 ) -> Result<bool, Box<dyn std::error::Error>> {
487 self.bootstrap_metadata_from_native_state()
488 }
489
490 pub(crate) fn native_state_is_bootstrap_complete(native_state: &NativePhysicalState) -> bool {
491 let registry_complete = native_state.registry.as_ref().map(|registry| {
492 registry.collections_complete
493 && registry.indexes_complete
494 && registry.graph_projections_complete
495 && registry.analytics_jobs_complete
496 && registry.vector_artifacts_complete
497 });
498 let recovery_complete = native_state
499 .recovery
500 .as_ref()
501 .map(|recovery| recovery.snapshots_complete && recovery.exports_complete);
502 let catalog_complete = native_state
503 .catalog
504 .as_ref()
505 .map(|catalog| catalog.collections_complete);
506
507 registry_complete == Some(true)
508 && recovery_complete == Some(true)
509 && catalog_complete == Some(true)
510 }
511
512 pub(crate) fn load_or_bootstrap_physical_metadata(
513 &self,
514 persist_bootstrapped: bool,
515 ) -> Result<PhysicalMetadataFile, Box<dyn std::error::Error>> {
516 if self.options.mode != StorageMode::Persistent {
517 return Err("physical metadata requires persistent mode".into());
518 }
519 let Some(path) = self.path() else {
520 return Err("database path is not available".into());
521 };
522 let native_state = self.native_physical_state();
523
524 match PhysicalMetadataFile::load_for_data_path(path) {
525 Ok(metadata) => {
526 if let Some(native_state) = native_state.as_ref() {
527 let inspection = Self::inspect_native_header_against_metadata(
528 native_state.header,
529 &metadata,
530 );
531 if Self::repair_policy_for_inspection(&inspection)
532 == NativeHeaderRepairPolicy::NativeAheadOfMetadata
533 {
534 let bootstrapped =
535 self.metadata_from_native_state(native_state, Some(&metadata));
536 if persist_bootstrapped && !self.options.read_only {
537 bootstrapped.save_for_data_path(path)?;
538 self.persist_native_physical_header(&bootstrapped)?;
539 }
540 return Ok(bootstrapped);
541 }
542 }
543 Ok(metadata)
544 }
545 Err(err) => {
546 if crate::physical::is_collection_contract_decode_error(&err) {
547 return Err(err.into());
548 }
549 let Some(native_state) = native_state else {
550 return Err(err.into());
551 };
552 let is_fresh_empty = native_state.header.sequence == 0
568 && native_state.registry.is_none()
569 && native_state.catalog.is_none()
570 && native_state.recovery.is_none();
571 if !is_fresh_empty && !Self::native_state_is_bootstrap_complete(&native_state) {
572 return Err(err.into());
573 }
574 let metadata = self.metadata_from_native_state(&native_state, None);
575 if persist_bootstrapped && !self.options.read_only {
576 metadata.save_for_data_path(path)?;
577 self.persist_native_physical_header(&metadata)?;
578 }
579 Ok(metadata)
580 }
581 }
582 }
583
584 pub(crate) fn physical_metadata_preference(&self) -> Option<&'static str> {
585 let path = self.path()?;
586 let native_state = self.native_physical_state();
587 let metadata = PhysicalMetadataFile::load_for_data_path(path).ok();
588
589 match (metadata, native_state) {
590 (Some(metadata), Some(native_state)) => {
591 let inspection =
592 Self::inspect_native_header_against_metadata(native_state.header, &metadata);
593 match Self::repair_policy_for_inspection(&inspection) {
594 NativeHeaderRepairPolicy::InSync => Some("sidecar_current"),
595 NativeHeaderRepairPolicy::RepairNativeFromMetadata => Some("sidecar_current"),
596 NativeHeaderRepairPolicy::NativeAheadOfMetadata => Some("native_ahead"),
597 }
598 }
599 (Some(_), None) => Some("sidecar_only"),
600 (None, Some(_)) => Some("sidecar_missing_native_available"),
601 (None, None) => Some("sidecar_missing_no_native"),
602 }
603 }
604
605 fn metadata_from_native_state(
606 &self,
607 native_state: &NativePhysicalState,
608 previous: Option<&PhysicalMetadataFile>,
609 ) -> PhysicalMetadataFile {
610 let now = SystemTime::now()
611 .duration_since(UNIX_EPOCH)
612 .unwrap_or_default()
613 .as_millis();
614 let catalog = self.catalog_snapshot();
615 let catalog_name = catalog.name.clone();
616 let catalog_total_entities = catalog.total_entities;
617 let catalog_total_collections = catalog.total_collections;
618 let indexes = self.physical_index_state();
619
620 let mut manifest =
621 crate::api::SchemaManifest::now(self.options.clone(), catalog.total_collections);
622 manifest.updated_at_unix_ms = now;
623
624 let manifest_events = native_state
625 .manifest
626 .as_ref()
627 .map(|summary| {
628 summary
629 .recent_events
630 .iter()
631 .map(|event| crate::physical::ManifestEvent {
632 collection: event.collection.clone(),
633 object_key: event.object_key.clone(),
634 kind: match event.kind.as_str() {
635 "insert" => crate::physical::ManifestEventKind::Insert,
636 "update" => crate::physical::ManifestEventKind::Update,
637 "remove" => crate::physical::ManifestEventKind::Remove,
638 _ => crate::physical::ManifestEventKind::Checkpoint,
639 },
640 block: crate::physical::BlockReference {
641 index: event.block_index,
642 checksum: event.block_checksum,
643 },
644 snapshot_min: event.snapshot_min,
645 snapshot_max: event.snapshot_max,
646 })
647 .collect()
648 })
649 .unwrap_or_default();
650
651 let graph_projections = native_state
652 .registry
653 .as_ref()
654 .and_then(|registry| {
655 registry.graph_projections_complete.then(|| {
656 registry
657 .graph_projections
658 .iter()
659 .map(|projection| crate::physical::PhysicalGraphProjection {
660 name: projection.name.clone(),
661 created_at_unix_ms: projection.created_at_unix_ms,
662 updated_at_unix_ms: projection.updated_at_unix_ms,
663 state: "materialized".to_string(),
664 source: projection.source.clone(),
665 node_labels: projection.node_labels.clone(),
666 node_types: projection.node_types.clone(),
667 edge_labels: projection.edge_labels.clone(),
668 last_materialized_sequence: projection.last_materialized_sequence,
669 })
670 .collect()
671 })
672 })
673 .or_else(|| previous.map(|metadata| metadata.graph_projections.clone()))
674 .unwrap_or_default();
675
676 let analytics_jobs = native_state
677 .registry
678 .as_ref()
679 .and_then(|registry| {
680 registry.analytics_jobs_complete.then(|| {
681 registry
682 .analytics_jobs
683 .iter()
684 .map(|job| crate::physical::PhysicalAnalyticsJob {
685 id: job.id.clone(),
686 kind: job.kind.clone(),
687 state: job.state.clone(),
688 projection: job.projection.clone(),
689 created_at_unix_ms: job.created_at_unix_ms,
690 updated_at_unix_ms: job.updated_at_unix_ms,
691 last_run_sequence: job.last_run_sequence,
692 metadata: job.metadata.clone(),
693 })
694 .collect()
695 })
696 })
697 .or_else(|| previous.map(|metadata| metadata.analytics_jobs.clone()))
698 .unwrap_or_default();
699
700 let exports = native_state
701 .recovery
702 .as_ref()
703 .and_then(|recovery| {
704 recovery.exports_complete.then(|| {
705 recovery
706 .exports
707 .iter()
708 .map(|export| crate::physical::ExportDescriptor {
709 name: export.name.clone(),
710 created_at_unix_ms: export.created_at_unix_ms,
711 snapshot_id: export.snapshot_id,
712 superblock_sequence: export.superblock_sequence,
713 data_path: self
714 .path()
715 .map(|path| {
716 crate::physical::PhysicalMetadataFile::export_data_path_for(
717 path,
718 &export.name,
719 )
720 .display()
721 .to_string()
722 })
723 .unwrap_or_default(),
724 metadata_path: self
725 .path()
726 .map(|path| {
727 let export_data_path =
728 crate::physical::PhysicalMetadataFile::export_data_path_for(
729 path,
730 &export.name,
731 );
732 crate::physical::PhysicalMetadataFile::metadata_path_for(
733 &export_data_path,
734 )
735 .display()
736 .to_string()
737 })
738 .unwrap_or_default(),
739 collection_count: export.collection_count as usize,
740 total_entities: export.total_entities as usize,
741 })
742 .collect()
743 })
744 })
745 .or_else(|| previous.map(|metadata| metadata.exports.clone()))
746 .unwrap_or_default();
747
748 let snapshots = native_state
749 .recovery
750 .as_ref()
751 .and_then(|recovery| {
752 recovery.snapshots_complete.then(|| {
753 recovery
754 .snapshots
755 .iter()
756 .map(|snapshot| crate::physical::SnapshotDescriptor {
757 snapshot_id: snapshot.snapshot_id,
758 created_at_unix_ms: snapshot.created_at_unix_ms,
759 superblock_sequence: snapshot.superblock_sequence,
760 collection_count: snapshot.collection_count as usize,
761 total_entities: snapshot.total_entities as usize,
762 })
763 .collect()
764 })
765 })
766 .or_else(|| previous.map(|metadata| metadata.snapshots.clone()))
767 .unwrap_or_else(|| {
768 vec![crate::physical::SnapshotDescriptor {
769 snapshot_id: native_state.header.sequence,
770 created_at_unix_ms: now,
771 superblock_sequence: native_state.header.sequence,
772 collection_count: catalog_total_collections,
773 total_entities: catalog_total_entities,
774 }]
775 });
776
777 let catalog_stats = native_state
778 .catalog
779 .as_ref()
780 .and_then(|native_catalog| {
781 native_catalog.collections_complete.then(|| {
782 native_catalog
783 .collections
784 .iter()
785 .map(|collection| {
786 (
787 collection.name.clone(),
788 crate::api::CollectionStats {
789 entities: collection.entities as usize,
790 cross_refs: collection.cross_refs as usize,
791 segments: collection.segments as usize,
792 },
793 )
794 })
795 .collect::<BTreeMap<_, _>>()
796 })
797 })
798 .or_else(|| previous.map(|metadata| metadata.catalog.stats_by_collection.clone()))
799 .unwrap_or_else(|| catalog.stats_by_collection.clone());
800
801 PhysicalMetadataFile {
802 protocol_version: crate::physical::PHYSICAL_METADATA_PROTOCOL_VERSION.to_string(),
803 generated_at_unix_ms: now,
804 last_loaded_from: Some("native_bootstrap".to_string()),
805 last_healed_at_unix_ms: Some(now),
806 manifest,
807 catalog: crate::api::CatalogSnapshot {
808 name: catalog_name,
809 total_entities: native_state
810 .catalog
811 .as_ref()
812 .map(|summary| summary.total_entities as usize)
813 .unwrap_or(catalog_total_entities),
814 total_collections: native_state
815 .catalog
816 .as_ref()
817 .map(|summary| summary.collection_count as usize)
818 .unwrap_or(catalog_total_collections),
819 stats_by_collection: catalog_stats,
820 updated_at: SystemTime::now(),
821 },
822 manifest_events,
823 collection_ttl_defaults_ms: previous
824 .map(|metadata| metadata.collection_ttl_defaults_ms.clone())
825 .unwrap_or_default(),
826 collection_contracts: previous
827 .map(|metadata| metadata.collection_contracts.clone())
828 .unwrap_or_default(),
829 hypertables: previous
830 .map(|metadata| metadata.hypertables.clone())
831 .unwrap_or_default(),
832 tree_definitions: previous
833 .map(|metadata| metadata.tree_definitions.clone())
834 .unwrap_or_default(),
835 indexes,
836 graph_projections,
837 analytics_jobs,
838 exports,
839 superblock: crate::physical::SuperblockHeader {
840 format_version: native_state.header.format_version,
841 sequence: native_state.header.sequence,
842 copies: crate::physical::DEFAULT_SUPERBLOCK_COPIES,
843 manifest: crate::physical::ManifestPointers {
844 oldest: crate::physical::BlockReference {
845 index: native_state.header.manifest_oldest_root,
846 checksum: 0,
847 },
848 newest: crate::physical::BlockReference {
849 index: native_state.header.manifest_root,
850 checksum: 0,
851 },
852 },
853 free_set: crate::physical::BlockReference {
854 index: native_state.header.free_set_root,
855 checksum: 0,
856 },
857 collection_roots: native_state.collection_roots.clone(),
858 },
859 snapshots,
860 }
861 }
862
863 pub(crate) fn reconcile_index_states_with_native_artifacts(
864 &self,
865 mut indexes: Vec<PhysicalIndexState>,
866 ) -> Vec<PhysicalIndexState> {
867 let native_artifacts = self
868 .native_physical_state()
869 .and_then(|state| state.registry)
870 .map(|registry| registry.vector_artifacts)
871 .unwrap_or_default();
872 for index in &mut indexes {
873 let Some(collection) = index.collection.as_deref() else {
874 continue;
875 };
876 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
877 continue;
878 };
879 let Some(artifact) = native_artifacts.iter().find(|artifact| {
880 artifact.collection == collection && artifact.artifact_kind == artifact_kind
881 }) else {
882 index.build_state = "metadata-only".to_string();
883 continue;
884 };
885 index.entries = artifact.vector_count as usize;
886 index.estimated_memory_bytes = artifact.serialized_bytes;
887 index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
888 index.artifact_kind = Some(artifact.artifact_kind.clone());
889 index.artifact_checksum = Some(artifact.checksum);
890 index.build_state = "artifact-published".to_string();
891 if let Some(pages) = self.native_vector_artifact_pages() {
892 index.artifact_root_page = pages
893 .into_iter()
894 .find(|page| {
895 page.collection == artifact.collection
896 && page.artifact_kind == artifact.artifact_kind
897 })
898 .map(|page| page.root_page);
899 }
900 }
901 indexes
902 }
903
904 pub(crate) fn warmup_native_vector_artifact_for_index(
905 &self,
906 index: &PhysicalIndexState,
907 ) -> Result<(), String> {
908 let Some(collection) = index.collection.as_deref() else {
909 return Ok(());
910 };
911 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
912 return Ok(());
913 };
914 self.warmup_native_vector_artifact(collection, Some(artifact_kind))?;
915 Ok(())
916 }
917
918 pub(crate) fn apply_runtime_native_artifact_to_index_state(
919 &self,
920 index: &mut PhysicalIndexState,
921 ) -> Result<(), String> {
922 let Some(collection) = index.collection.as_deref() else {
923 return Ok(());
924 };
925 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
926 return Ok(());
927 };
928 let artifact = self.inspect_native_vector_artifact(collection, Some(artifact_kind))?;
929 index.entries = artifact
930 .graph_edge_count
931 .or(artifact.text_posting_count)
932 .unwrap_or(artifact.node_count) as usize;
933 index.estimated_memory_bytes = artifact.byte_len;
934 index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
935 index.artifact_kind = Some(artifact.artifact_kind.clone());
936 index.artifact_checksum = Some(artifact.checksum);
937 index.build_state = "ready".to_string();
938 index.artifact_root_page = self
939 .native_vector_artifact_pages()
940 .and_then(|pages| {
941 pages.into_iter().find(|page| {
942 page.collection == artifact.collection
943 && page.artifact_kind == artifact.artifact_kind
944 })
945 })
946 .map(|page| page.root_page);
947 Ok(())
948 }
949
950 pub(crate) fn physical_index_state_from_native_state(
951 &self,
952 native_state: &NativePhysicalState,
953 previous: Option<&PhysicalMetadataFile>,
954 ) -> Vec<PhysicalIndexState> {
955 let mut fresh = self.physical_index_state();
956 let Some(registry) = native_state.registry.as_ref() else {
957 if let Some(previous) = previous {
958 for index in &previous.indexes {
959 if !fresh.iter().any(|candidate| candidate.name == index.name) {
960 fresh.push(index.clone());
961 }
962 }
963 }
964 return fresh;
965 };
966
967 for index in &mut fresh {
968 if let Some(native) = registry
969 .indexes
970 .iter()
971 .find(|candidate| candidate.name == index.name)
972 {
973 index.enabled = native.enabled;
974 index.last_refresh_ms = native.last_refresh_ms;
975 index.backend = native.backend.clone();
976 index.entries = native.entries as usize;
977 index.estimated_memory_bytes = native.estimated_memory_bytes;
978 if index.artifact_kind.is_none() {
979 index.artifact_kind = Self::native_artifact_kind_for_index(index.kind)
980 .map(|value| value.to_string());
981 }
982 if index.build_state == "catalog-derived" {
983 index.build_state = "registry-loaded".to_string();
984 }
985 }
986 }
987
988 for native in ®istry.indexes {
989 if fresh.iter().any(|index| index.name == native.name) {
990 continue;
991 }
992 let Some(kind) = Self::index_kind_from_str(&native.kind) else {
993 continue;
994 };
995 fresh.push(PhysicalIndexState {
996 name: native.name.clone(),
997 kind,
998 collection: native.collection.clone(),
999 enabled: native.enabled,
1000 entries: native.entries as usize,
1001 estimated_memory_bytes: native.estimated_memory_bytes,
1002 last_refresh_ms: native.last_refresh_ms,
1003 backend: native.backend.clone(),
1004 artifact_kind: Self::native_artifact_kind_for_index(kind)
1005 .map(|value| value.to_string()),
1006 artifact_root_page: None,
1007 artifact_checksum: None,
1008 build_state: "registry-loaded".to_string(),
1009 });
1010 }
1011
1012 if !registry.indexes_complete {
1013 if let Some(previous) = previous {
1014 for index in &previous.indexes {
1015 if !fresh.iter().any(|candidate| candidate.name == index.name) {
1016 fresh.push(index.clone());
1017 }
1018 }
1019 }
1020 }
1021
1022 fresh
1023 }
1024
1025 pub(crate) fn graph_projections_from_native_state(
1026 &self,
1027 native_state: &NativePhysicalState,
1028 ) -> Vec<PhysicalGraphProjection> {
1029 native_state
1030 .registry
1031 .as_ref()
1032 .map(|registry| {
1033 registry
1034 .graph_projections
1035 .iter()
1036 .map(|projection| PhysicalGraphProjection {
1037 name: projection.name.clone(),
1038 created_at_unix_ms: projection.created_at_unix_ms,
1039 updated_at_unix_ms: projection.updated_at_unix_ms,
1040 state: "materialized".to_string(),
1041 source: projection.source.clone(),
1042 node_labels: projection.node_labels.clone(),
1043 node_types: projection.node_types.clone(),
1044 edge_labels: projection.edge_labels.clone(),
1045 last_materialized_sequence: projection.last_materialized_sequence,
1046 })
1047 .collect()
1048 })
1049 .unwrap_or_default()
1050 }
1051
1052 pub(crate) fn analytics_jobs_from_native_state(
1053 &self,
1054 native_state: &NativePhysicalState,
1055 ) -> Vec<PhysicalAnalyticsJob> {
1056 native_state
1057 .registry
1058 .as_ref()
1059 .map(|registry| {
1060 registry
1061 .analytics_jobs
1062 .iter()
1063 .map(|job| PhysicalAnalyticsJob {
1064 id: job.id.clone(),
1065 kind: job.kind.clone(),
1066 state: job.state.clone(),
1067 projection: job.projection.clone(),
1068 created_at_unix_ms: job.created_at_unix_ms,
1069 updated_at_unix_ms: job.updated_at_unix_ms,
1070 last_run_sequence: job.last_run_sequence,
1071 metadata: job.metadata.clone(),
1072 })
1073 .collect()
1074 })
1075 .unwrap_or_default()
1076 }
1077
1078 pub(crate) fn exports_from_native_state(
1079 &self,
1080 native_state: &NativePhysicalState,
1081 ) -> Vec<ExportDescriptor> {
1082 native_state
1083 .recovery
1084 .as_ref()
1085 .map(|recovery| {
1086 recovery
1087 .exports
1088 .iter()
1089 .map(|export| ExportDescriptor {
1090 name: export.name.clone(),
1091 created_at_unix_ms: export.created_at_unix_ms,
1092 snapshot_id: export.snapshot_id,
1093 superblock_sequence: export.superblock_sequence,
1094 data_path: self
1095 .path()
1096 .map(|path| {
1097 crate::physical::PhysicalMetadataFile::export_data_path_for(
1098 path,
1099 &export.name,
1100 )
1101 .display()
1102 .to_string()
1103 })
1104 .unwrap_or_default(),
1105 metadata_path: self
1106 .path()
1107 .map(|path| {
1108 let export_data_path =
1109 crate::physical::PhysicalMetadataFile::export_data_path_for(
1110 path,
1111 &export.name,
1112 );
1113 crate::physical::PhysicalMetadataFile::metadata_path_for(
1114 &export_data_path,
1115 )
1116 .display()
1117 .to_string()
1118 })
1119 .unwrap_or_default(),
1120 collection_count: export.collection_count as usize,
1121 total_entities: export.total_entities as usize,
1122 })
1123 .collect()
1124 })
1125 .unwrap_or_default()
1126 }
1127
1128 pub(crate) fn snapshots_from_native_state(
1129 &self,
1130 native_state: &NativePhysicalState,
1131 ) -> Vec<crate::physical::SnapshotDescriptor> {
1132 let snapshots: Vec<_> = native_state
1133 .recovery
1134 .as_ref()
1135 .map(|recovery| {
1136 recovery
1137 .snapshots
1138 .iter()
1139 .map(|snapshot| crate::physical::SnapshotDescriptor {
1140 snapshot_id: snapshot.snapshot_id,
1141 created_at_unix_ms: snapshot.created_at_unix_ms,
1142 superblock_sequence: snapshot.superblock_sequence,
1143 collection_count: snapshot.collection_count as usize,
1144 total_entities: snapshot.total_entities as usize,
1145 })
1146 .collect()
1147 })
1148 .unwrap_or_default();
1149 if !snapshots.is_empty() {
1150 return snapshots;
1151 }
1152
1153 let now = SystemTime::now()
1154 .duration_since(UNIX_EPOCH)
1155 .unwrap_or_default()
1156 .as_millis();
1157 let (collection_count, total_entities) = native_state
1158 .catalog
1159 .as_ref()
1160 .map(|catalog| {
1161 (
1162 catalog.collection_count as usize,
1163 catalog.total_entities as usize,
1164 )
1165 })
1166 .unwrap_or_else(|| {
1167 let catalog = self.catalog_snapshot();
1168 (catalog.total_collections, catalog.total_entities)
1169 });
1170
1171 vec![crate::physical::SnapshotDescriptor {
1172 snapshot_id: native_state.header.sequence,
1173 created_at_unix_ms: now,
1174 superblock_sequence: native_state.header.sequence,
1175 collection_count,
1176 total_entities,
1177 }]
1178 }
1179
1180 fn index_kind_from_str(value: &str) -> Option<crate::index::IndexKind> {
1181 match value {
1182 "btree" => Some(crate::index::IndexKind::BTree),
1183 "vector.hnsw" => Some(crate::index::IndexKind::VectorHnsw),
1184 "vector.inverted" => Some(crate::index::IndexKind::VectorInverted),
1185 "vector.turbo" => Some(crate::index::IndexKind::VectorTurbo),
1186 "graph.adjacency" => Some(crate::index::IndexKind::GraphAdjacency),
1187 "text.fulltext" => Some(crate::index::IndexKind::FullText),
1188 "document.pathvalue" => Some(crate::index::IndexKind::DocumentPathValue),
1189 "search.hybrid" => Some(crate::index::IndexKind::HybridSearch),
1190 _ => None,
1191 }
1192 }
1193
1194 pub(crate) fn native_artifact_kind_for_index(kind: IndexKind) -> Option<&'static str> {
1195 match kind {
1196 IndexKind::VectorHnsw => Some("hnsw"),
1197 IndexKind::VectorInverted => Some("ivf"),
1198 IndexKind::VectorTurbo => Some("turboquant"),
1199 IndexKind::GraphAdjacency => Some("graph.adjacency"),
1200 IndexKind::FullText => Some("text.fulltext"),
1201 IndexKind::DocumentPathValue => Some("document.pathvalue"),
1202 _ => None,
1203 }
1204 }
1205
1206 fn index_is_declared(&self, name: &str) -> bool {
1207 self.physical_metadata()
1208 .map(|metadata| metadata.indexes.iter().any(|index| index.name == name))
1209 .unwrap_or(false)
1210 }
1211
1212 pub(crate) fn graph_projection_is_declared(&self, name: &str) -> bool {
1213 self.physical_metadata()
1214 .map(|metadata| {
1215 metadata
1216 .graph_projections
1217 .iter()
1218 .any(|projection| projection.name == name)
1219 })
1220 .unwrap_or(false)
1221 }
1222
1223 pub(crate) fn graph_projection_is_operational(&self, name: &str) -> bool {
1224 self.operational_graph_projections()
1225 .into_iter()
1226 .any(|projection| projection.name == name && projection.state == "materialized")
1227 }
1228
1229 pub(crate) fn analytics_job_id(kind: &str, projection: Option<&str>) -> String {
1230 match projection {
1231 Some(projection) => format!("{kind}::{projection}"),
1232 None => format!("{kind}::global"),
1233 }
1234 }
1235
1236 pub(crate) fn update_physical_metadata<T, F>(
1237 &self,
1238 mutator: F,
1239 ) -> Result<T, Box<dyn std::error::Error>>
1240 where
1241 F: FnOnce(&mut PhysicalMetadataFile) -> T,
1242 {
1243 if self.options.mode != StorageMode::Persistent {
1244 return Err("physical metadata operations require persistent mode".into());
1245 }
1246 if self.options.read_only {
1247 return Err("physical metadata operations are not allowed in read-only mode".into());
1248 }
1249 let Some(path) = self.path() else {
1250 return Err("database path is not available".into());
1251 };
1252
1253 let mut metadata = self.load_or_bootstrap_physical_metadata(true)?;
1254
1255 if metadata.indexes.is_empty() {
1256 metadata.indexes = self.physical_index_state();
1257 }
1258 metadata.superblock.collection_roots = self.physical_collection_roots();
1259
1260 let result = mutator(&mut metadata);
1261 metadata.save_for_data_path(path)?;
1262 self.persist_native_physical_header(&metadata)?;
1263 Ok(result)
1264 }
1265
1266 pub(crate) fn persist_native_physical_header(
1267 &self,
1268 metadata: &PhysicalMetadataFile,
1269 ) -> Result<(), Box<dyn std::error::Error>> {
1270 if !self.paged_mode {
1271 return Ok(());
1272 }
1273
1274 let existing_page = self
1275 .store
1276 .physical_file_header()
1277 .map(|header| header.collection_roots_page)
1278 .filter(|page| *page != 0);
1279 let existing_registry_page = self
1280 .store
1281 .physical_file_header()
1282 .map(|header| header.registry_page)
1283 .filter(|page| *page != 0);
1284 let existing_recovery_page = self
1285 .store
1286 .physical_file_header()
1287 .map(|header| header.recovery_page)
1288 .filter(|page| *page != 0);
1289 let existing_catalog_page = self
1290 .store
1291 .physical_file_header()
1292 .map(|header| header.catalog_page)
1293 .filter(|page| *page != 0);
1294 let existing_metadata_state_page = self
1295 .store
1296 .physical_file_header()
1297 .map(|header| header.metadata_state_page)
1298 .filter(|page| *page != 0);
1299 let existing_vector_artifact_page = self
1300 .store
1301 .physical_file_header()
1302 .map(|header| header.vector_artifact_page)
1303 .filter(|page| *page != 0);
1304 let existing_manifest_page = self
1305 .store
1306 .physical_file_header()
1307 .map(|header| header.manifest_page)
1308 .filter(|page| *page != 0);
1309 let (manifest_page, manifest_checksum) = self.store.write_native_manifest_summary(
1310 metadata.superblock.sequence,
1311 &metadata.manifest_events,
1312 existing_manifest_page,
1313 )?;
1314 let (collection_roots_page, collection_roots_checksum) = self
1315 .store
1316 .write_native_collection_roots(&metadata.superblock.collection_roots, existing_page)?;
1317 let registry_summary = self.native_registry_summary_from_metadata(metadata);
1318 let (registry_page, registry_checksum) = self
1319 .store
1320 .write_native_registry_summary(®istry_summary, existing_registry_page)?;
1321 let recovery_summary = Self::native_recovery_summary_from_metadata(metadata);
1322 let (recovery_page, recovery_checksum) = self
1323 .store
1324 .write_native_recovery_summary(&recovery_summary, existing_recovery_page)?;
1325 let catalog_summary = Self::native_catalog_summary_from_metadata(metadata);
1326 let (catalog_page, catalog_checksum) = self
1327 .store
1328 .write_native_catalog_summary(&catalog_summary, existing_catalog_page)?;
1329 let metadata_state_summary = Self::native_metadata_state_summary_from_metadata(metadata);
1330 let (metadata_state_page, metadata_state_checksum) =
1331 self.store.write_native_metadata_state_summary(
1332 &metadata_state_summary,
1333 existing_metadata_state_page,
1334 )?;
1335 let vector_artifact_records = self.native_vector_artifact_records();
1336 let vector_artifact_payloads = vector_artifact_records
1337 .iter()
1338 .map(|(summary, bytes)| {
1339 (
1340 summary.collection.clone(),
1341 summary.artifact_kind.clone(),
1342 bytes.clone(),
1343 )
1344 })
1345 .collect::<Vec<_>>();
1346 let (vector_artifact_page, vector_artifact_checksum, _vector_artifact_pages) =
1347 self.store.write_native_vector_artifact_store(
1348 &vector_artifact_payloads,
1349 existing_vector_artifact_page,
1350 )?;
1351 let mut header = Self::native_header_from_metadata(metadata);
1352 header.manifest_page = manifest_page;
1353 header.manifest_checksum = manifest_checksum;
1354 header.collection_roots_page = collection_roots_page;
1355 header.collection_roots_checksum = collection_roots_checksum;
1356 header.registry_page = registry_page;
1357 header.registry_checksum = registry_checksum;
1358 header.recovery_page = recovery_page;
1359 header.recovery_checksum = recovery_checksum;
1360 header.catalog_page = catalog_page;
1361 header.catalog_checksum = catalog_checksum;
1362 header.metadata_state_page = metadata_state_page;
1363 header.metadata_state_checksum = metadata_state_checksum;
1364 header.vector_artifact_page = vector_artifact_page;
1365 header.vector_artifact_checksum = vector_artifact_checksum;
1366 self.store.update_physical_file_header(header)?;
1367 self.store.persist()?;
1368 Ok(())
1369 }
1370
1371 pub(crate) fn native_header_from_metadata(
1372 metadata: &PhysicalMetadataFile,
1373 ) -> PhysicalFileHeader {
1374 PhysicalFileHeader {
1375 format_version: metadata.superblock.format_version,
1376 sequence: metadata.superblock.sequence,
1377 manifest_oldest_root: metadata.superblock.manifest.oldest.index,
1378 manifest_root: metadata.superblock.manifest.newest.index,
1379 free_set_root: metadata.superblock.free_set.index,
1380 manifest_page: 0,
1381 manifest_checksum: 0,
1382 collection_roots_page: 0,
1383 collection_roots_checksum: 0,
1384 collection_root_count: metadata.superblock.collection_roots.len() as u32,
1385 snapshot_count: metadata.snapshots.len() as u32,
1386 index_count: metadata.indexes.len() as u32,
1387 catalog_collection_count: metadata.catalog.total_collections as u32,
1388 catalog_total_entities: metadata.catalog.total_entities as u64,
1389 export_count: metadata.exports.len() as u32,
1390 graph_projection_count: metadata.graph_projections.len() as u32,
1391 analytics_job_count: metadata.analytics_jobs.len() as u32,
1392 manifest_event_count: metadata.manifest_events.len() as u32,
1393 registry_page: 0,
1394 registry_checksum: 0,
1395 recovery_page: 0,
1396 recovery_checksum: 0,
1397 catalog_page: 0,
1398 catalog_checksum: 0,
1399 metadata_state_page: 0,
1400 metadata_state_checksum: 0,
1401 vector_artifact_page: 0,
1402 vector_artifact_checksum: 0,
1403 }
1404 }
1405
1406 fn recover_queue_pending_state(&self) {
1407 const QUEUE_META_COLLECTION: &str = "red_queue_meta";
1408
1409 let Some(manager) = self.store.get_collection(QUEUE_META_COLLECTION) else {
1410 return;
1411 };
1412
1413 let pending_rows = manager.query_all(|entity| {
1414 entity.data.as_row().is_some_and(|row| {
1415 matches!(
1416 row.get_field("kind"),
1417 Some(reddb_types::Value::Text(kind))
1418 if matches!(&**kind, "queue_pending" | "queue_pending_lc")
1419 )
1420 })
1421 });
1422
1423 for row in pending_rows {
1424 let _ = self.store.delete(QUEUE_META_COLLECTION, row.id);
1425 }
1426 }
1427}
1428
1429fn current_unix_ms() -> u64 {
1430 SystemTime::now()
1431 .duration_since(UNIX_EPOCH)
1432 .unwrap_or_default()
1433 .as_millis()
1434 .min(u128::from(u64::MAX)) as u64
1435}