1use super::*;
2use crate::storage::unified::metadata::MetadataValue;
3
4const PHYSICAL_METADATA_KEY: &str = "physical";
5
6#[cfg(test)]
7fn metadata_qualification_phase(started: &std::time::Instant, name: &str) {
8 if std::env::var_os("REDDB_QUALIFICATION_CHECKPOINT_PHASES").is_some() {
9 eprintln!(
10 "runtime qualification phase=checkpoint_metadata name={name} elapsed_us={}",
11 started.elapsed().as_micros()
12 );
13 }
14}
15
16impl RedDB {
17 pub(crate) fn persist_physical_metadata_record(
20 &self,
21 metadata: &PhysicalMetadataFile,
22 ) -> Result<(), Box<dyn std::error::Error>> {
23 if self.options.read_only {
24 return Err("physical metadata operations are not allowed in read-only mode".into());
25 }
26 let value = crate::serde_json::Value::String(metadata.to_document_json(false)?);
27 self.store
28 .try_set_storage_metadata(PHYSICAL_METADATA_KEY, &value)?;
29 Ok(())
30 }
31
32 fn load_internal_physical_metadata(
33 &self,
34 ) -> Result<Option<PhysicalMetadataFile>, Box<dyn std::error::Error>> {
35 let Some(value) = self.store.try_get_storage_metadata(PHYSICAL_METADATA_KEY)? else {
36 return Ok(None);
37 };
38 let Value::Text(document) = value else {
39 return Err(std::io::Error::new(
40 std::io::ErrorKind::InvalidData,
41 "invalid internal physical metadata record",
42 )
43 .into());
44 };
45 Ok(Some(PhysicalMetadataFile::from_document_json(
46 &document,
47 "invalid internal physical metadata record",
48 )?))
49 }
50
51 fn has_legacy_metadata_sidecars(&self) -> std::io::Result<bool> {
52 let Some(path) = self.path() else {
53 return Ok(false);
54 };
55 Ok(
56 PhysicalMetadataFile::metadata_binary_path_for(path).try_exists()?
57 || PhysicalMetadataFile::metadata_path_for(path).try_exists()?
58 || !PhysicalMetadataFile::journal_paths_for_data_path(path)?.is_empty(),
59 )
60 }
61
62 pub(crate) fn remove_imported_metadata_sidecars(
65 &self,
66 path: &Path,
67 ) -> Result<(), Box<dyn std::error::Error>> {
68 if self.options.read_only
69 || !self.has_legacy_metadata_sidecars()?
70 || self.load_internal_physical_metadata()?.is_none()
71 {
72 return Ok(());
73 }
74 reddb_file::physical_metadata::remove_imported_physical_metadata_sidecars(path)?;
75 Ok(())
76 }
77
78 pub fn is_replica_role(&self) -> bool {
79 matches!(
80 self.options.replication.role,
81 crate::replication::ReplicationRole::Replica { .. }
82 )
83 }
84
85 pub fn enforce_retention_policy(&self) -> Result<(), Box<dyn std::error::Error>> {
86 if self.options.read_only || self.is_replica_role() {
87 return Ok(());
88 }
89
90 if self.options.mode == StorageMode::Persistent {
93 let Some(_) = self.path() else {
94 return Ok(());
95 };
96
97 let _publication = self.store.vault_publication_lock.lock();
98 let Ok(mut metadata) = self.load_or_bootstrap_physical_metadata(true) else {
99 return Ok(());
100 };
101
102 self.prune_export_registry(&mut metadata.exports);
103 self.persist_physical_metadata_record(&metadata)?;
104 }
105
106 Ok(())
107 }
108
109 pub fn entity_visible_at_read(
110 &self,
111 collection: &str,
112 entity: &crate::storage::UnifiedEntity,
113 ) -> bool {
114 if self
115 .store
116 .get_collection(collection)
117 .is_some_and(|manager| {
118 manager.metadata_expiry_projection_ready()
119 && manager.scheduled_metadata_expiry_count() == 0
120 })
121 {
122 return true;
123 }
124 !self.entity_expired_at(collection, entity, current_unix_ms())
125 }
126
127 pub(crate) fn entity_expired_at(
128 &self,
129 collection: &str,
130 entity: &crate::storage::UnifiedEntity,
131 now_ms: u64,
132 ) -> bool {
133 match self.try_entity_expired_at(collection, entity, now_ms) {
134 Ok(expired) => expired,
135 Err(error) => {
136 tracing::warn!(%error, collection, entity_id = entity.id.raw(), "TTL metadata read failed closed");
137 true
138 }
139 }
140 }
141
142 pub(crate) fn try_entity_expired_at(
143 &self,
144 collection: &str,
145 entity: &crate::storage::UnifiedEntity,
146 now_ms: u64,
147 ) -> Result<bool, crate::storage::StoreError> {
148 let Some(metadata) = self
149 .store
150 .try_get_metadata(collection, entity.id, usize::MAX)?
151 else {
152 return Ok(false);
153 };
154
155 Ok(Self::entity_metadata_expired_at(entity, &metadata, now_ms))
156 }
157
158 pub(crate) fn entity_metadata_expired_at(
159 entity: &crate::storage::UnifiedEntity,
160 metadata: &crate::storage::unified::metadata::Metadata,
161 now_ms: u64,
162 ) -> bool {
163 if metadata
164 .get("_expires_at")
165 .and_then(Self::metadata_u64)
166 .is_some_and(|expires_at_ms| expires_at_ms <= now_ms)
167 {
168 return true;
169 }
170
171 let ttl_ms = metadata.get("_ttl_ms").and_then(Self::metadata_u64);
172 let ttl_secs = if ttl_ms.is_none() {
173 metadata.get("_ttl").and_then(|value| {
174 Self::metadata_u64(value).and_then(|value_secs| value_secs.checked_mul(1000))
175 })
176 } else {
177 None
178 };
179
180 let Some(ttl_ms) = ttl_ms.or(ttl_secs) else {
181 return false;
182 };
183 entity
184 .created_at
185 .saturating_mul(1000)
186 .saturating_add(ttl_ms)
187 <= now_ms
188 }
189
190 fn metadata_u64(value: &MetadataValue) -> Option<u64> {
191 match value {
192 MetadataValue::Int(v) if *v >= 0 => Some(*v as u64),
193 MetadataValue::Timestamp(v) => Some(*v),
194 MetadataValue::Float(v) => {
195 if !v.is_finite() || !v.is_sign_positive() || v.fract().abs() >= f64::EPSILON {
196 return None;
197 }
198 if *v > u64::MAX as f64 {
199 return None;
200 }
201 Some(v.trunc() as u64)
202 }
203 MetadataValue::String(v) => v.parse::<u64>().ok(),
204 _ => None,
205 }
206 }
207
208 pub fn node(&self, collection: impl Into<String>, label: impl Into<String>) -> NodeBuilder {
221 NodeBuilder::new(
222 self.store.clone(),
223 self.preprocessors.clone(),
224 collection,
225 label,
226 )
227 .with_runtime(self.runtime.get().cloned())
228 }
229
230 pub fn edge(&self, collection: impl Into<String>, label: impl Into<String>) -> EdgeBuilder {
242 EdgeBuilder::new(
243 self.store.clone(),
244 self.preprocessors.clone(),
245 collection,
246 label,
247 )
248 .with_runtime(self.runtime.get().cloned())
249 }
250
251 pub fn vector(&self, collection: impl Into<String>) -> VectorBuilder {
262 VectorBuilder::new(self.store.clone(), self.preprocessors.clone(), collection)
263 }
264
265 pub fn row(&self, table: impl Into<String>, columns: Vec<(&str, Value)>) -> RowBuilder {
276 RowBuilder::new(
277 self.store.clone(),
278 self.preprocessors.clone(),
279 table,
280 columns,
281 )
282 }
283
284 pub fn doc(&self, collection: impl Into<String>) -> DocumentBuilder {
298 DocumentBuilder::new(self.store.clone(), self.preprocessors.clone(), collection)
299 }
300
301 pub fn kv(
312 &self,
313 collection: impl Into<String>,
314 key: impl Into<String>,
315 value: Value,
316 ) -> KvBuilder {
317 KvBuilder::new(
318 self.store.clone(),
319 self.preprocessors.clone(),
320 collection,
321 key,
322 value,
323 )
324 }
325
326 pub fn get_kv(&self, collection: &str, key: &str) -> Option<(Value, EntityId)> {
331 if UnifiedStore::canonical_collection(collection) == "red.config" {
332 let entity = self.store.try_get_config_entity(key).ok().flatten()?;
333 let value = entity
334 .data
335 .as_row()
336 .and_then(|row| row.get_field("value"))
337 .cloned()
338 .unwrap_or(Value::Null);
339 return Some((value, entity.id));
340 }
341 let manager = self.store.get_collection(collection)?;
342 if let Some(value) = manager.project_key_entity(key, None, false, |entity| {
343 (
344 entity
345 .data
346 .as_row()
347 .and_then(|row| row.get_field("value"))
348 .cloned()
349 .unwrap_or(Value::Null),
350 entity.id,
351 )
352 }) {
353 return Some(value);
354 }
355 let columns = manager.column_schema();
356 let key_index = columns
357 .as_ref()
358 .and_then(|names| names.iter().position(|name| name == "key"));
359 let value_index = columns
360 .as_ref()
361 .and_then(|names| names.iter().position(|name| name == "value"));
362 let mut found = None;
363 manager.for_each_entity(|entity| {
364 let EntityData::Row(row) = &entity.data else {
365 return true;
366 };
367 let legacy = row.named.is_none() && row.schema.is_none();
368 let stored_key = row.get_field("key").or_else(|| {
369 if legacy {
370 key_index.and_then(|index| row.columns.get(index))
371 } else {
372 None
373 }
374 });
375 if matches!(stored_key, Some(Value::Text(stored)) if stored.as_ref() == key) {
376 let value = row.get_field("value").or_else(|| {
377 if legacy {
378 value_index.and_then(|index| row.columns.get(index))
379 } else {
380 None
381 }
382 });
383 found = Some((value.cloned().unwrap_or(Value::Null), entity.id));
384 return false;
385 }
386 true
387 });
388 found
389 }
390
391 pub fn delete_kv(
393 &self,
394 collection: &str,
395 key: &str,
396 ) -> Result<bool, super::super::error::DevXError> {
397 let Some((_, id)) = self.get_kv(collection, key) else {
398 return Ok(false);
399 };
400 self.store
401 .delete(collection, id)
402 .map_err(|err| super::super::error::DevXError::Storage(format!("{err:?}")))?;
403 Ok(true)
404 }
405
406 pub(crate) fn with_initialized_metadata(self) -> Result<Self, Box<dyn std::error::Error>> {
407 if self.options.mode == StorageMode::Persistent {
408 let has_internal_metadata = self.load_internal_physical_metadata()?.is_some();
410 match self.load_or_bootstrap_physical_metadata(false) {
412 Ok(metadata) => {
413 crate::reserved_fields::validate_physical_metadata_contracts(&metadata)
414 .map_err(|err| err.to_string())?;
415 if !has_internal_metadata && !self.options.read_only {
416 self.persist_physical_metadata_record(&metadata)?;
417 }
418 }
419 Err(error)
420 if error
421 .downcast_ref::<std::io::Error>()
422 .is_some_and(crate::physical::is_collection_contract_decode_error) =>
423 {
424 return Err(error);
425 }
426 Err(error) => {
427 if self.has_legacy_metadata_sidecars()? {
428 return Err(error);
429 }
430 }
431 }
432 }
436 self.load_collection_ttl_defaults_from_metadata();
437 if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
444 && self.options.storage_profile.packaging
445 == crate::storage::StoragePackaging::SingleFile
446 && !self.paged_mode
447 {
448 self.seed_contract_cache_from_store_aux()?;
449 }
450 self.load_hypertables_from_metadata();
454 Ok(self)
458 }
459
460 pub(crate) fn persist_metadata(&self) -> Result<(), Box<dyn std::error::Error>> {
461 if self.options.mode != StorageMode::Persistent || self.options.read_only {
462 return Ok(());
463 }
464 if !self.paged_mode
467 && self.options.storage_profile.deploy_profile
468 == crate::storage::DeployProfile::Embedded
469 && self.options.storage_profile.packaging
470 == crate::storage::StoragePackaging::SingleFile
471 {
472 return Ok(());
473 }
474 let Some(_) = self.path() else {
475 return Ok(());
476 };
477
478 #[cfg(test)]
479 let qualification_started = std::time::Instant::now();
480 let _publication = self.store.vault_publication_lock.lock();
481 #[cfg(test)]
482 metadata_qualification_phase(&qualification_started, "publication-lock");
483 let previous = self.load_or_bootstrap_physical_metadata(false).ok();
484 #[cfg(test)]
485 metadata_qualification_phase(&qualification_started, "previous-metadata");
486 let collection_roots = self.physical_collection_roots();
487 #[cfg(test)]
488 metadata_qualification_phase(&qualification_started, "collection-roots");
489 let native_state = self.native_physical_state();
490 #[cfg(test)]
491 metadata_qualification_phase(&qualification_started, "native-state");
492 let mut indexes = native_state
493 .map(|state| self.physical_index_state_from_native_state(&state, previous.as_ref()))
494 .unwrap_or_else(|| self.physical_index_state());
495 if let Some(previous) = previous.as_ref() {
496 let absent = |name: &str| {
500 !collection_roots.contains_key(name)
501 && !previous
502 .collection_contracts
503 .iter()
504 .any(|contract| contract.name == name)
505 };
506 let mut removed: BTreeSet<&str> = previous
507 .superblock
508 .collection_roots
509 .keys()
510 .map(String::as_str)
511 .filter(|name| absent(name))
512 .collect();
513 let mut seen = BTreeSet::new();
514 for event in previous.manifest_events.iter().rev() {
515 if seen.insert(event.collection.as_str())
516 && matches!(&event.kind, crate::physical::ManifestEventKind::Remove)
517 && absent(&event.collection)
518 {
519 removed.insert(event.collection.as_str());
520 }
521 }
522 indexes.retain(|index| {
523 !index
524 .collection
525 .as_deref()
526 .is_some_and(|collection| removed.contains(collection))
527 });
528 }
529 #[cfg(test)]
530 metadata_qualification_phase(&qualification_started, "index-state");
531 let mut metadata = PhysicalMetadataFile::from_state(
532 self.options.clone(),
533 self.catalog_snapshot(),
534 collection_roots,
535 indexes,
536 previous.as_ref(),
537 );
538 metadata.collection_ttl_defaults_ms = self.collection_ttl_defaults_snapshot();
539 metadata.hypertables = self.hypertable_registry_snapshot();
543 #[cfg(test)]
544 metadata_qualification_phase(&qualification_started, "metadata-assembled");
545 self.persist_native_physical_header(&metadata)?;
546 #[cfg(test)]
547 metadata_qualification_phase(&qualification_started, "native-header");
548 Ok(())
549 }
550
551 fn bootstrap_metadata_from_native_state(&self) -> Result<bool, Box<dyn std::error::Error>> {
552 if self.options.mode != StorageMode::Persistent || self.options.read_only {
553 return Ok(false);
554 }
555 let Some(_) = self.path() else {
556 return Ok(false);
557 };
558 let Some(native_state) = self.native_physical_state() else {
559 return Ok(false);
560 };
561 if !Self::native_state_is_bootstrap_complete(&native_state) {
562 return Ok(false);
563 }
564
565 let _publication = self.store.vault_publication_lock.lock();
566 let previous = self.load_or_bootstrap_physical_metadata(false).ok();
567 let metadata = self.metadata_from_native_state(&native_state, previous.as_ref());
568 self.persist_native_physical_header(&metadata)?;
569 Ok(true)
570 }
571
572 pub fn rebuild_physical_metadata_from_native_state(
575 &self,
576 ) -> Result<bool, Box<dyn std::error::Error>> {
577 self.bootstrap_metadata_from_native_state()
578 }
579
580 pub(crate) fn native_state_is_bootstrap_complete(native_state: &NativePhysicalState) -> bool {
581 let registry_complete = native_state.registry.as_ref().map(|registry| {
582 registry.collections_complete
583 && registry.indexes_complete
584 && registry.graph_projections_complete
585 && registry.analytics_jobs_complete
586 && registry.vector_artifacts_complete
587 });
588 let recovery_complete = native_state
589 .recovery
590 .as_ref()
591 .map(|recovery| recovery.snapshots_complete && recovery.exports_complete);
592 let catalog_complete = native_state
593 .catalog
594 .as_ref()
595 .map(|catalog| catalog.collections_complete);
596
597 registry_complete == Some(true)
598 && recovery_complete == Some(true)
599 && catalog_complete == Some(true)
600 }
601
602 fn native_state_is_summary_free_legacy(native_state: &NativePhysicalState) -> bool {
603 let header = native_state.header;
604 header.manifest_page == 0
605 && header.manifest_checksum == 0
606 && header.registry_page == 0
607 && header.registry_checksum == 0
608 && header.recovery_page == 0
609 && header.recovery_checksum == 0
610 && header.catalog_page == 0
611 && header.catalog_checksum == 0
612 && header.metadata_state_page == 0
613 && header.metadata_state_checksum == 0
614 && header.vector_artifact_page == 0
615 && header.vector_artifact_checksum == 0
616 && native_state.manifest.is_none()
617 && native_state.registry.is_none()
618 && native_state.recovery.is_none()
619 && native_state.catalog.is_none()
620 && native_state.metadata_state.is_none()
621 && native_state.vector_artifact_pages.is_none()
622 }
623
624 pub(crate) fn load_or_bootstrap_physical_metadata(
625 &self,
626 persist_bootstrapped: bool,
627 ) -> Result<PhysicalMetadataFile, Box<dyn std::error::Error>> {
628 if self.options.mode != StorageMode::Persistent {
629 return Err("physical metadata requires persistent mode".into());
630 }
631 let Some(path) = self.path() else {
632 return Err("database path is not available".into());
633 };
634 let _publication = self.store.vault_publication_lock.lock();
635 let internal_metadata = match self.load_internal_physical_metadata()? {
636 Some(metadata) => {
637 let native_ahead = self.store.physical_file_header().is_some_and(|header| {
641 Self::repair_policy_for_inspection(
642 &Self::inspect_native_header_against_metadata(header, &metadata),
643 ) == NativeHeaderRepairPolicy::NativeAheadOfMetadata
644 });
645 if !native_ahead {
646 return Ok(metadata);
647 }
648 Some(metadata)
649 }
650 None => None,
651 };
652 let native_state = self.native_physical_state();
653 let metadata = match internal_metadata {
654 Some(metadata) => Ok(metadata),
655 None => PhysicalMetadataFile::inspect_for_data_path(path),
656 };
657 match metadata {
658 Ok(metadata) => {
659 if let Some(native_state) = native_state.as_ref() {
660 let inspection = Self::inspect_native_header_against_metadata(
661 native_state.header,
662 &metadata,
663 );
664 if Self::repair_policy_for_inspection(&inspection)
665 == NativeHeaderRepairPolicy::NativeAheadOfMetadata
666 {
667 let bootstrapped =
668 self.metadata_from_native_state(native_state, Some(&metadata));
669 if persist_bootstrapped && !self.options.read_only {
670 self.persist_native_physical_header(&bootstrapped)?;
671 }
672 return Ok(bootstrapped);
673 }
674 }
675 Ok(metadata)
676 }
677 Err(err) => {
678 if crate::physical::is_collection_contract_decode_error(&err)
679 || self.has_legacy_metadata_sidecars()?
680 {
681 return Err(err.into());
682 }
683 let Some(native_state) = native_state else {
684 return Err(err.into());
685 };
686 if !Self::native_state_is_summary_free_legacy(&native_state)
693 && !Self::native_state_is_bootstrap_complete(&native_state)
694 {
695 return Err(err.into());
696 }
697 let metadata = self.metadata_from_native_state(&native_state, None);
698 if persist_bootstrapped && !self.options.read_only {
699 self.persist_native_physical_header(&metadata)?;
700 }
701 Ok(metadata)
702 }
703 }
704 }
705
706 pub(crate) fn physical_metadata_preference(&self) -> Option<&'static str> {
707 if self
708 .load_internal_physical_metadata()
709 .ok()
710 .flatten()
711 .is_some()
712 {
713 return Some("internal_record");
714 }
715 let path = self.path()?;
716 let native_state = self.native_physical_state();
717 let metadata = PhysicalMetadataFile::inspect_for_data_path(path).ok();
718
719 match (metadata, native_state) {
720 (Some(metadata), Some(native_state)) => {
721 let inspection =
722 Self::inspect_native_header_against_metadata(native_state.header, &metadata);
723 match Self::repair_policy_for_inspection(&inspection) {
724 NativeHeaderRepairPolicy::InSync => Some("sidecar_current"),
725 NativeHeaderRepairPolicy::RepairNativeFromMetadata => Some("sidecar_current"),
726 NativeHeaderRepairPolicy::NativeAheadOfMetadata => Some("native_ahead"),
727 }
728 }
729 (Some(_), None) => Some("sidecar_only"),
730 (None, Some(_)) => Some("sidecar_missing_native_available"),
731 (None, None) => Some("sidecar_missing_no_native"),
732 }
733 }
734
735 fn metadata_from_native_state(
736 &self,
737 native_state: &NativePhysicalState,
738 previous: Option<&PhysicalMetadataFile>,
739 ) -> PhysicalMetadataFile {
740 let now = SystemTime::now()
741 .duration_since(UNIX_EPOCH)
742 .unwrap_or_default()
743 .as_millis();
744 let catalog = self.catalog_snapshot();
745 let catalog_name = catalog.name.clone();
746 let catalog_total_entities = catalog.total_entities;
747 let catalog_total_collections = catalog.total_collections;
748 let indexes = self.physical_index_state_from_native_state(native_state, previous);
749
750 let mut manifest =
751 crate::api::SchemaManifest::now(self.options.clone(), catalog.total_collections);
752 manifest.updated_at_unix_ms = now;
753
754 let manifest_events = native_state
755 .manifest
756 .as_ref()
757 .map(|summary| {
758 summary
759 .recent_events
760 .iter()
761 .map(|event| crate::physical::ManifestEvent {
762 collection: event.collection.clone(),
763 object_key: event.object_key.clone(),
764 kind: match event.kind.as_str() {
765 "insert" => crate::physical::ManifestEventKind::Insert,
766 "update" => crate::physical::ManifestEventKind::Update,
767 "remove" => crate::physical::ManifestEventKind::Remove,
768 _ => crate::physical::ManifestEventKind::Checkpoint,
769 },
770 block: crate::physical::BlockReference {
771 index: event.block_index,
772 checksum: event.block_checksum,
773 },
774 snapshot_min: event.snapshot_min,
775 snapshot_max: event.snapshot_max,
776 })
777 .collect()
778 })
779 .unwrap_or_default();
780
781 let graph_projections = native_state
782 .registry
783 .as_ref()
784 .and_then(|registry| {
785 registry.graph_projections_complete.then(|| {
786 registry
787 .graph_projections
788 .iter()
789 .map(|projection| crate::physical::PhysicalGraphProjection {
790 name: projection.name.clone(),
791 created_at_unix_ms: projection.created_at_unix_ms,
792 updated_at_unix_ms: projection.updated_at_unix_ms,
793 state: "materialized".to_string(),
794 source: projection.source.clone(),
795 node_labels: projection.node_labels.clone(),
796 node_types: projection.node_types.clone(),
797 edge_labels: projection.edge_labels.clone(),
798 last_materialized_sequence: projection.last_materialized_sequence,
799 })
800 .collect()
801 })
802 })
803 .or_else(|| previous.map(|metadata| metadata.graph_projections.clone()))
804 .unwrap_or_default();
805
806 let analytics_jobs = native_state
807 .registry
808 .as_ref()
809 .and_then(|registry| {
810 registry.analytics_jobs_complete.then(|| {
811 registry
812 .analytics_jobs
813 .iter()
814 .map(|job| crate::physical::PhysicalAnalyticsJob {
815 id: job.id.clone(),
816 kind: job.kind.clone(),
817 state: job.state.clone(),
818 projection: job.projection.clone(),
819 created_at_unix_ms: job.created_at_unix_ms,
820 updated_at_unix_ms: job.updated_at_unix_ms,
821 last_run_sequence: job.last_run_sequence,
822 metadata: job.metadata.clone(),
823 })
824 .collect()
825 })
826 })
827 .or_else(|| previous.map(|metadata| metadata.analytics_jobs.clone()))
828 .unwrap_or_default();
829
830 let exports = native_state
831 .recovery
832 .as_ref()
833 .and_then(|recovery| {
834 recovery.exports_complete.then(|| {
835 recovery
836 .exports
837 .iter()
838 .map(|export| crate::physical::ExportDescriptor {
839 name: export.name.clone(),
840 created_at_unix_ms: export.created_at_unix_ms,
841 snapshot_id: export.snapshot_id,
842 superblock_sequence: export.superblock_sequence,
843 data_path: self
844 .path()
845 .map(|path| {
846 crate::physical::PhysicalMetadataFile::export_data_path_for(
847 path,
848 &export.name,
849 )
850 .display()
851 .to_string()
852 })
853 .unwrap_or_default(),
854 metadata_path: self
855 .path()
856 .map(|path| {
857 let export_data_path =
858 crate::physical::PhysicalMetadataFile::export_data_path_for(
859 path,
860 &export.name,
861 );
862 crate::physical::PhysicalMetadataFile::metadata_path_for(
863 &export_data_path,
864 )
865 .display()
866 .to_string()
867 })
868 .unwrap_or_default(),
869 collection_count: export.collection_count as usize,
870 total_entities: export.total_entities as usize,
871 })
872 .collect()
873 })
874 })
875 .or_else(|| previous.map(|metadata| metadata.exports.clone()))
876 .unwrap_or_default();
877
878 let snapshots = native_state
879 .recovery
880 .as_ref()
881 .and_then(|recovery| {
882 recovery.snapshots_complete.then(|| {
883 recovery
884 .snapshots
885 .iter()
886 .map(|snapshot| crate::physical::SnapshotDescriptor {
887 snapshot_id: snapshot.snapshot_id,
888 created_at_unix_ms: snapshot.created_at_unix_ms,
889 superblock_sequence: snapshot.superblock_sequence,
890 collection_count: snapshot.collection_count as usize,
891 total_entities: snapshot.total_entities as usize,
892 })
893 .collect()
894 })
895 })
896 .or_else(|| previous.map(|metadata| metadata.snapshots.clone()))
897 .unwrap_or_else(|| {
898 vec![crate::physical::SnapshotDescriptor {
899 snapshot_id: native_state.header.sequence,
900 created_at_unix_ms: now,
901 superblock_sequence: native_state.header.sequence,
902 collection_count: catalog_total_collections,
903 total_entities: catalog_total_entities,
904 }]
905 });
906
907 let catalog_stats = native_state
908 .catalog
909 .as_ref()
910 .and_then(|native_catalog| {
911 native_catalog.collections_complete.then(|| {
912 native_catalog
913 .collections
914 .iter()
915 .map(|collection| {
916 (
917 collection.name.clone(),
918 crate::api::CollectionStats {
919 entities: collection.entities as usize,
920 cross_refs: collection.cross_refs as usize,
921 segments: collection.segments as usize,
922 },
923 )
924 })
925 .collect::<BTreeMap<_, _>>()
926 })
927 })
928 .or_else(|| previous.map(|metadata| metadata.catalog.stats_by_collection.clone()))
929 .unwrap_or_else(|| catalog.stats_by_collection.clone());
930
931 PhysicalMetadataFile {
932 protocol_version: crate::physical::PHYSICAL_METADATA_PROTOCOL_VERSION.to_string(),
933 generated_at_unix_ms: now,
934 last_loaded_from: Some("native_bootstrap".to_string()),
935 last_healed_at_unix_ms: Some(now),
936 manifest,
937 catalog: crate::api::CatalogSnapshot {
938 name: catalog_name,
939 total_entities: native_state
940 .catalog
941 .as_ref()
942 .map(|summary| summary.total_entities as usize)
943 .unwrap_or(catalog_total_entities),
944 total_collections: native_state
945 .catalog
946 .as_ref()
947 .map(|summary| summary.collection_count as usize)
948 .unwrap_or(catalog_total_collections),
949 stats_by_collection: catalog_stats,
950 updated_at: SystemTime::now(),
951 },
952 manifest_events,
953 collection_ttl_defaults_ms: previous
954 .map(|metadata| metadata.collection_ttl_defaults_ms.clone())
955 .unwrap_or_default(),
956 collection_contracts: previous
957 .map(|metadata| metadata.collection_contracts.clone())
958 .unwrap_or_default(),
959 hypertables: previous
960 .map(|metadata| metadata.hypertables.clone())
961 .unwrap_or_default(),
962 tree_definitions: previous
963 .map(|metadata| metadata.tree_definitions.clone())
964 .unwrap_or_default(),
965 indexes,
966 graph_projections,
967 analytics_jobs,
968 exports,
969 superblock: crate::physical::SuperblockHeader {
970 format_version: native_state.header.format_version,
971 sequence: native_state.header.sequence,
972 copies: crate::physical::DEFAULT_SUPERBLOCK_COPIES,
973 manifest: crate::physical::ManifestPointers {
974 oldest: crate::physical::BlockReference {
975 index: native_state.header.manifest_oldest_root,
976 checksum: 0,
977 },
978 newest: crate::physical::BlockReference {
979 index: native_state.header.manifest_root,
980 checksum: 0,
981 },
982 },
983 free_set: crate::physical::BlockReference {
984 index: native_state.header.free_set_root,
985 checksum: 0,
986 },
987 collection_roots: native_state.collection_roots.clone(),
988 },
989 snapshots,
990 }
991 }
992
993 pub(crate) fn reconcile_index_states_with_native_artifacts(
994 &self,
995 mut indexes: Vec<PhysicalIndexState>,
996 ) -> Vec<PhysicalIndexState> {
997 let native_state = self.native_physical_state();
998 let native_artifacts = native_state
999 .as_ref()
1000 .and_then(|state| state.registry.as_ref())
1001 .map(|registry| registry.vector_artifacts.as_slice())
1002 .unwrap_or_default();
1003 let artifact_pages = native_state
1004 .as_ref()
1005 .and_then(|state| state.vector_artifact_pages.as_ref());
1006 for index in &mut indexes {
1007 let Some(collection) = index.collection.as_deref() else {
1008 continue;
1009 };
1010 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
1011 continue;
1012 };
1013 let Some(artifact) = native_artifacts.iter().find(|artifact| {
1014 artifact.collection == collection && artifact.artifact_kind == artifact_kind
1015 }) else {
1016 index.build_state = if index.backend == "memory-budget-deferred" {
1017 "deferred-memory-budget"
1018 } else {
1019 "metadata-only"
1020 }
1021 .to_string();
1022 continue;
1023 };
1024 index.entries = artifact.vector_count as usize;
1025 index.estimated_memory_bytes = artifact.serialized_bytes;
1026 index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
1027 index.artifact_kind = Some(artifact.artifact_kind.clone());
1028 index.artifact_checksum = Some(artifact.checksum);
1029 index.build_state = "artifact-published".to_string();
1030 if let Some(pages) = artifact_pages {
1031 index.artifact_root_page = pages
1032 .iter()
1033 .find(|page| {
1034 page.collection == artifact.collection
1035 && page.artifact_kind == artifact.artifact_kind
1036 })
1037 .map(|page| page.root_page);
1038 }
1039 }
1040 indexes
1041 }
1042
1043 pub(crate) fn warmup_native_vector_artifact_for_index(
1044 &self,
1045 index: &PhysicalIndexState,
1046 ) -> Result<(), String> {
1047 let Some(collection) = index.collection.as_deref() else {
1048 return Ok(());
1049 };
1050 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
1051 return Ok(());
1052 };
1053 self.warmup_native_vector_artifact(collection, Some(artifact_kind))?;
1054 Ok(())
1055 }
1056
1057 pub(crate) fn apply_runtime_native_artifact_to_index_state(
1058 &self,
1059 index: &mut PhysicalIndexState,
1060 ) -> Result<(), String> {
1061 let Some(collection) = index.collection.as_deref() else {
1062 return Ok(());
1063 };
1064 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
1065 return Ok(());
1066 };
1067 let artifact = self.inspect_native_vector_artifact(collection, Some(artifact_kind))?;
1068 index.entries = artifact
1069 .graph_edge_count
1070 .or(artifact.text_posting_count)
1071 .unwrap_or(artifact.node_count) as usize;
1072 index.estimated_memory_bytes = artifact.byte_len;
1073 index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
1074 index.artifact_kind = Some(artifact.artifact_kind.clone());
1075 index.artifact_checksum = Some(artifact.checksum);
1076 index.build_state = "ready".to_string();
1077 index.artifact_root_page = self
1078 .native_vector_artifact_pages()
1079 .and_then(|pages| {
1080 pages.into_iter().find(|page| {
1081 page.collection == artifact.collection
1082 && page.artifact_kind == artifact.artifact_kind
1083 })
1084 })
1085 .map(|page| page.root_page);
1086 Ok(())
1087 }
1088
1089 pub(crate) fn physical_index_state_from_native_state(
1090 &self,
1091 native_state: &NativePhysicalState,
1092 previous: Option<&PhysicalMetadataFile>,
1093 ) -> Vec<PhysicalIndexState> {
1094 let mut fresh = self.physical_index_state();
1095 let Some(registry) = native_state.registry.as_ref() else {
1096 if let Some(previous) = previous {
1097 for index in &previous.indexes {
1098 if !fresh.iter().any(|candidate| candidate.name == index.name) {
1099 fresh.push(index.clone());
1100 }
1101 }
1102 }
1103 return fresh;
1104 };
1105
1106 for index in &mut fresh {
1107 if let Some(native) = registry
1108 .indexes
1109 .iter()
1110 .find(|candidate| candidate.name == index.name)
1111 {
1112 index.enabled = native.enabled;
1113 index.last_refresh_ms = native.last_refresh_ms;
1114 index.backend = native.backend.clone();
1115 index.entries = native.entries as usize;
1116 index.estimated_memory_bytes = native.estimated_memory_bytes;
1117 if index.artifact_kind.is_none() {
1118 index.artifact_kind = Self::native_artifact_kind_for_index(index.kind)
1119 .map(|value| value.to_string());
1120 }
1121 if index.build_state == "catalog-derived" {
1122 index.build_state = "registry-loaded".to_string();
1123 }
1124 }
1125 if index.backend == "memory-budget-deferred"
1126 || previous.is_some_and(|metadata| {
1127 metadata.indexes.iter().any(|previous| {
1128 previous.name == index.name && previous.backend == "memory-budget-deferred"
1129 })
1130 })
1131 {
1132 index.backend = "memory-budget-deferred".to_string();
1133 index.build_state = "deferred-memory-budget".to_string();
1134 index.artifact_root_page = None;
1135 index.artifact_checksum = None;
1136 }
1137 }
1138
1139 for native in ®istry.indexes {
1140 if fresh.iter().any(|index| index.name == native.name) {
1141 continue;
1142 }
1143 let Some(kind) = Self::index_kind_from_str(&native.kind) else {
1144 continue;
1145 };
1146 fresh.push(PhysicalIndexState {
1147 name: native.name.clone(),
1148 kind,
1149 collection: native.collection.clone(),
1150 enabled: native.enabled,
1151 entries: native.entries as usize,
1152 estimated_memory_bytes: native.estimated_memory_bytes,
1153 last_refresh_ms: native.last_refresh_ms,
1154 backend: native.backend.clone(),
1155 artifact_kind: Self::native_artifact_kind_for_index(kind)
1156 .map(|value| value.to_string()),
1157 artifact_root_page: None,
1158 artifact_checksum: None,
1159 build_state: "registry-loaded".to_string(),
1160 });
1161 }
1162
1163 if !registry.indexes_complete {
1164 if let Some(previous) = previous {
1165 for index in &previous.indexes {
1166 if !fresh.iter().any(|candidate| candidate.name == index.name) {
1167 fresh.push(index.clone());
1168 }
1169 }
1170 }
1171 }
1172
1173 fresh
1174 }
1175
1176 pub(crate) fn graph_projections_from_native_state(
1177 &self,
1178 native_state: &NativePhysicalState,
1179 ) -> Vec<PhysicalGraphProjection> {
1180 native_state
1181 .registry
1182 .as_ref()
1183 .map(|registry| {
1184 registry
1185 .graph_projections
1186 .iter()
1187 .map(|projection| PhysicalGraphProjection {
1188 name: projection.name.clone(),
1189 created_at_unix_ms: projection.created_at_unix_ms,
1190 updated_at_unix_ms: projection.updated_at_unix_ms,
1191 state: "materialized".to_string(),
1192 source: projection.source.clone(),
1193 node_labels: projection.node_labels.clone(),
1194 node_types: projection.node_types.clone(),
1195 edge_labels: projection.edge_labels.clone(),
1196 last_materialized_sequence: projection.last_materialized_sequence,
1197 })
1198 .collect()
1199 })
1200 .unwrap_or_default()
1201 }
1202
1203 pub(crate) fn analytics_jobs_from_native_state(
1204 &self,
1205 native_state: &NativePhysicalState,
1206 ) -> Vec<PhysicalAnalyticsJob> {
1207 native_state
1208 .registry
1209 .as_ref()
1210 .map(|registry| {
1211 registry
1212 .analytics_jobs
1213 .iter()
1214 .map(|job| PhysicalAnalyticsJob {
1215 id: job.id.clone(),
1216 kind: job.kind.clone(),
1217 state: job.state.clone(),
1218 projection: job.projection.clone(),
1219 created_at_unix_ms: job.created_at_unix_ms,
1220 updated_at_unix_ms: job.updated_at_unix_ms,
1221 last_run_sequence: job.last_run_sequence,
1222 metadata: job.metadata.clone(),
1223 })
1224 .collect()
1225 })
1226 .unwrap_or_default()
1227 }
1228
1229 pub(crate) fn exports_from_native_state(
1230 &self,
1231 native_state: &NativePhysicalState,
1232 ) -> Vec<ExportDescriptor> {
1233 native_state
1234 .recovery
1235 .as_ref()
1236 .map(|recovery| {
1237 recovery
1238 .exports
1239 .iter()
1240 .map(|export| ExportDescriptor {
1241 name: export.name.clone(),
1242 created_at_unix_ms: export.created_at_unix_ms,
1243 snapshot_id: export.snapshot_id,
1244 superblock_sequence: export.superblock_sequence,
1245 data_path: self
1246 .path()
1247 .map(|path| {
1248 crate::physical::PhysicalMetadataFile::export_data_path_for(
1249 path,
1250 &export.name,
1251 )
1252 .display()
1253 .to_string()
1254 })
1255 .unwrap_or_default(),
1256 metadata_path: self
1257 .path()
1258 .map(|path| {
1259 let export_data_path =
1260 crate::physical::PhysicalMetadataFile::export_data_path_for(
1261 path,
1262 &export.name,
1263 );
1264 crate::physical::PhysicalMetadataFile::metadata_path_for(
1265 &export_data_path,
1266 )
1267 .display()
1268 .to_string()
1269 })
1270 .unwrap_or_default(),
1271 collection_count: export.collection_count as usize,
1272 total_entities: export.total_entities as usize,
1273 })
1274 .collect()
1275 })
1276 .unwrap_or_default()
1277 }
1278
1279 pub(crate) fn snapshots_from_native_state(
1280 &self,
1281 native_state: &NativePhysicalState,
1282 ) -> Vec<crate::physical::SnapshotDescriptor> {
1283 let snapshots: Vec<_> = native_state
1284 .recovery
1285 .as_ref()
1286 .map(|recovery| {
1287 recovery
1288 .snapshots
1289 .iter()
1290 .map(|snapshot| crate::physical::SnapshotDescriptor {
1291 snapshot_id: snapshot.snapshot_id,
1292 created_at_unix_ms: snapshot.created_at_unix_ms,
1293 superblock_sequence: snapshot.superblock_sequence,
1294 collection_count: snapshot.collection_count as usize,
1295 total_entities: snapshot.total_entities as usize,
1296 })
1297 .collect()
1298 })
1299 .unwrap_or_default();
1300 if !snapshots.is_empty() {
1301 return snapshots;
1302 }
1303
1304 let now = SystemTime::now()
1305 .duration_since(UNIX_EPOCH)
1306 .unwrap_or_default()
1307 .as_millis();
1308 let (collection_count, total_entities) = native_state
1309 .catalog
1310 .as_ref()
1311 .map(|catalog| {
1312 (
1313 catalog.collection_count as usize,
1314 catalog.total_entities as usize,
1315 )
1316 })
1317 .unwrap_or_else(|| {
1318 let catalog = self.catalog_snapshot();
1319 (catalog.total_collections, catalog.total_entities)
1320 });
1321
1322 vec![crate::physical::SnapshotDescriptor {
1323 snapshot_id: native_state.header.sequence,
1324 created_at_unix_ms: now,
1325 superblock_sequence: native_state.header.sequence,
1326 collection_count,
1327 total_entities,
1328 }]
1329 }
1330
1331 fn index_kind_from_str(value: &str) -> Option<crate::index::IndexKind> {
1332 match value {
1333 "btree" => Some(crate::index::IndexKind::BTree),
1334 "vector.hnsw" => Some(crate::index::IndexKind::VectorHnsw),
1335 "vector.inverted" => Some(crate::index::IndexKind::VectorInverted),
1336 "vector.turbo" => Some(crate::index::IndexKind::VectorTurbo),
1337 "graph.adjacency" => Some(crate::index::IndexKind::GraphAdjacency),
1338 "text.fulltext" => Some(crate::index::IndexKind::FullText),
1339 "document.pathvalue" => Some(crate::index::IndexKind::DocumentPathValue),
1340 "search.hybrid" => Some(crate::index::IndexKind::HybridSearch),
1341 _ => None,
1342 }
1343 }
1344
1345 pub(crate) fn native_artifact_kind_for_index(kind: IndexKind) -> Option<&'static str> {
1346 match kind {
1347 IndexKind::VectorHnsw => Some("hnsw"),
1348 IndexKind::VectorInverted => Some("ivf"),
1349 IndexKind::VectorTurbo => Some("turboquant"),
1350 IndexKind::GraphAdjacency => Some("graph.adjacency"),
1351 IndexKind::FullText => Some("text.fulltext"),
1352 IndexKind::DocumentPathValue => Some("document.pathvalue"),
1353 _ => None,
1354 }
1355 }
1356
1357 fn index_is_declared(&self, name: &str) -> bool {
1358 self.physical_metadata()
1359 .map(|metadata| metadata.indexes.iter().any(|index| index.name == name))
1360 .unwrap_or(false)
1361 }
1362
1363 pub(crate) fn graph_projection_is_declared(&self, name: &str) -> bool {
1364 self.physical_metadata()
1365 .map(|metadata| {
1366 metadata
1367 .graph_projections
1368 .iter()
1369 .any(|projection| projection.name == name)
1370 })
1371 .unwrap_or(false)
1372 }
1373
1374 pub(crate) fn graph_projection_is_operational(&self, name: &str) -> bool {
1375 self.operational_graph_projections()
1376 .into_iter()
1377 .any(|projection| projection.name == name && projection.state == "materialized")
1378 }
1379
1380 pub(crate) fn analytics_job_id(kind: &str, projection: Option<&str>) -> String {
1381 match projection {
1382 Some(projection) => format!("{kind}::{projection}"),
1383 None => format!("{kind}::global"),
1384 }
1385 }
1386
1387 pub(crate) fn update_physical_metadata<T, F>(
1388 &self,
1389 mutator: F,
1390 ) -> Result<T, Box<dyn std::error::Error>>
1391 where
1392 F: FnOnce(&mut PhysicalMetadataFile) -> T,
1393 {
1394 if self.options.mode != StorageMode::Persistent {
1395 return Err("physical metadata operations require persistent mode".into());
1396 }
1397 if self.options.read_only {
1398 return Err("physical metadata operations are not allowed in read-only mode".into());
1399 }
1400 let Some(_) = self.path() else {
1401 return Err("database path is not available".into());
1402 };
1403
1404 let _publication = self.store.vault_publication_lock.lock();
1411 let mut metadata = self.load_or_bootstrap_physical_metadata(true)?;
1412
1413 if metadata.indexes.is_empty() {
1414 metadata.indexes = self
1415 .native_physical_state()
1416 .map(|state| self.physical_index_state_from_native_state(&state, Some(&metadata)))
1417 .unwrap_or_else(|| self.physical_index_state());
1418 }
1419 metadata.superblock.collection_roots = self.physical_collection_roots();
1420
1421 let result = mutator(&mut metadata);
1422 self.persist_native_physical_header(&metadata)?;
1423 Ok(result)
1424 }
1425
1426 pub(crate) fn persist_native_physical_header(
1427 &self,
1428 metadata: &PhysicalMetadataFile,
1429 ) -> Result<(), Box<dyn std::error::Error>> {
1430 if !self.paged_mode {
1431 return self.persist_physical_metadata_record(metadata);
1432 }
1433
1434 let _publication = self.store.vault_publication_lock.lock();
1435 let existing_page = self
1436 .store
1437 .physical_file_header()
1438 .map(|header| header.collection_roots_page)
1439 .filter(|page| *page != 0);
1440 let existing_registry_page = self
1441 .store
1442 .physical_file_header()
1443 .map(|header| header.registry_page)
1444 .filter(|page| *page != 0);
1445 let existing_recovery_page = self
1446 .store
1447 .physical_file_header()
1448 .map(|header| header.recovery_page)
1449 .filter(|page| *page != 0);
1450 let existing_catalog_page = self
1451 .store
1452 .physical_file_header()
1453 .map(|header| header.catalog_page)
1454 .filter(|page| *page != 0);
1455 let existing_metadata_state_page = self
1456 .store
1457 .physical_file_header()
1458 .map(|header| header.metadata_state_page)
1459 .filter(|page| *page != 0);
1460 let existing_vector_artifact_page = self
1461 .store
1462 .physical_file_header()
1463 .map(|header| header.vector_artifact_page)
1464 .filter(|page| *page != 0);
1465 let existing_manifest_page = self
1466 .store
1467 .physical_file_header()
1468 .map(|header| header.manifest_page)
1469 .filter(|page| *page != 0);
1470 let (manifest_page, manifest_checksum) = self.store.write_native_manifest_summary(
1471 metadata.superblock.sequence,
1472 &metadata.manifest_events,
1473 existing_manifest_page,
1474 )?;
1475 let (collection_roots_page, collection_roots_checksum) = self
1476 .store
1477 .write_native_collection_roots(&metadata.superblock.collection_roots, existing_page)?;
1478 let build = self.native_vector_artifact_records()?;
1481 let vector_artifact_records = build.records;
1482 let mut metadata = metadata.clone();
1483 for index in &mut metadata.indexes {
1484 let Some(kind) = Self::native_artifact_kind_for_index(index.kind) else {
1485 continue;
1486 };
1487 let Some(collection) = &index.collection else {
1488 continue;
1489 };
1490 if build.deferred_collections.contains(collection) {
1491 index.backend = "memory-budget-deferred".to_string();
1492 index.build_state = "deferred-memory-budget".to_string();
1493 index.artifact_kind = Some(kind.to_string());
1494 index.artifact_root_page = None;
1495 index.artifact_checksum = None;
1496 } else if let Some((summary, _)) =
1497 vector_artifact_records.iter().find(|(summary, _)| {
1498 &summary.collection == collection && summary.artifact_kind == kind
1499 })
1500 {
1501 index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
1502 index.build_state = "artifact-published".to_string();
1503 index.artifact_kind = Some(kind.to_string());
1504 index.artifact_checksum = Some(summary.checksum);
1505 }
1506 }
1507 let metadata = &metadata;
1508 let artifact_summaries = vector_artifact_records
1509 .iter()
1510 .map(|(summary, _)| summary.clone())
1511 .collect::<Vec<_>>();
1512 let registry_summary = self.native_registry_summary_from_metadata(
1513 metadata,
1514 &artifact_summaries,
1515 artifact_summaries.len(),
1516 );
1517 let (registry_page, registry_checksum) = self
1518 .store
1519 .write_native_registry_summary(®istry_summary, existing_registry_page)?;
1520 let recovery_summary = Self::native_recovery_summary_from_metadata(metadata);
1521 let (recovery_page, recovery_checksum) = self
1522 .store
1523 .write_native_recovery_summary(&recovery_summary, existing_recovery_page)?;
1524 let catalog_summary = Self::native_catalog_summary_from_metadata(metadata);
1525 let (catalog_page, catalog_checksum) = self
1526 .store
1527 .write_native_catalog_summary(&catalog_summary, existing_catalog_page)?;
1528 let metadata_state_summary = Self::native_metadata_state_summary_from_metadata(metadata);
1529 let (metadata_state_page, metadata_state_checksum) =
1530 self.store.write_native_metadata_state_summary(
1531 &metadata_state_summary,
1532 existing_metadata_state_page,
1533 )?;
1534 let vector_artifact_payloads = vector_artifact_records
1535 .iter()
1536 .map(|(summary, bytes)| {
1537 (
1538 summary.collection.clone(),
1539 summary.artifact_kind.clone(),
1540 bytes.clone(),
1541 )
1542 })
1543 .collect::<Vec<_>>();
1544 let (vector_artifact_page, vector_artifact_checksum, _vector_artifact_pages) =
1545 self.store.write_native_vector_artifact_store(
1546 &vector_artifact_payloads,
1547 existing_vector_artifact_page,
1548 )?;
1549 let mut header = Self::native_header_from_metadata(metadata);
1550 header.manifest_page = manifest_page;
1551 header.manifest_checksum = manifest_checksum;
1552 header.collection_roots_page = collection_roots_page;
1553 header.collection_roots_checksum = collection_roots_checksum;
1554 header.registry_page = registry_page;
1555 header.registry_checksum = registry_checksum;
1556 header.recovery_page = recovery_page;
1557 header.recovery_checksum = recovery_checksum;
1558 header.catalog_page = catalog_page;
1559 header.catalog_checksum = catalog_checksum;
1560 header.metadata_state_page = metadata_state_page;
1561 header.metadata_state_checksum = metadata_state_checksum;
1562 header.vector_artifact_page = vector_artifact_page;
1563 header.vector_artifact_checksum = vector_artifact_checksum;
1564 self.persist_physical_metadata_record(metadata)?;
1565 self.store.update_physical_file_header(header)?;
1566 self.store.persist()?;
1567 self.store.publish_detached_native_blob_pages()?;
1568 Ok(())
1569 }
1570
1571 pub(crate) fn native_header_from_metadata(
1572 metadata: &PhysicalMetadataFile,
1573 ) -> PhysicalFileHeader {
1574 PhysicalFileHeader {
1575 format_version: metadata.superblock.format_version,
1576 sequence: metadata.superblock.sequence,
1577 manifest_oldest_root: metadata.superblock.manifest.oldest.index,
1578 manifest_root: metadata.superblock.manifest.newest.index,
1579 free_set_root: metadata.superblock.free_set.index,
1580 manifest_page: 0,
1581 manifest_checksum: 0,
1582 collection_roots_page: 0,
1583 collection_roots_checksum: 0,
1584 collection_root_count: metadata.superblock.collection_roots.len() as u32,
1585 snapshot_count: metadata.snapshots.len() as u32,
1586 index_count: metadata.indexes.len() as u32,
1587 catalog_collection_count: metadata.catalog.total_collections as u32,
1588 catalog_total_entities: metadata.catalog.total_entities as u64,
1589 export_count: metadata.exports.len() as u32,
1590 graph_projection_count: metadata.graph_projections.len() as u32,
1591 analytics_job_count: metadata.analytics_jobs.len() as u32,
1592 manifest_event_count: metadata.manifest_events.len() as u32,
1593 registry_page: 0,
1594 registry_checksum: 0,
1595 recovery_page: 0,
1596 recovery_checksum: 0,
1597 catalog_page: 0,
1598 catalog_checksum: 0,
1599 metadata_state_page: 0,
1600 metadata_state_checksum: 0,
1601 vector_artifact_page: 0,
1602 vector_artifact_checksum: 0,
1603 }
1604 }
1605}
1606
1607fn current_unix_ms() -> u64 {
1608 SystemTime::now()
1609 .duration_since(UNIX_EPOCH)
1610 .unwrap_or_default()
1611 .as_millis()
1612 .min(u128::from(u64::MAX)) as u64
1613}
1614
1615#[cfg(test)]
1616mod catalog_observation_tests {
1617 use super::*;
1618
1619 #[test]
1620 fn summary_free_legacy_native_state_rebuilds_a_missing_sidecar_after_catalog_publication() {
1621 let directory = tempfile::tempdir().expect("temporary database");
1622 let path = directory.path().join("legacy-summary-free.rdb");
1623 let options = RedDBOptions::persistent(&path)
1624 .with_memory_budget(64 * 1024 * 1024)
1625 .with_storage_profile(crate::StorageDeployPreset::PrimaryReplicaDev.selection())
1626 .expect("paged profile");
1627 let runtime = crate::RedDBRuntime::with_options(options).expect("paged runtime");
1628 runtime
1629 .execute_query("CREATE TABLE retained (value INTEGER)")
1630 .expect("retained table");
1631 runtime
1632 .execute_query("INSERT INTO retained (value) VALUES (17)")
1633 .expect("retained row");
1634 runtime.checkpoint().expect("publish retained row");
1635
1636 let db = runtime.db();
1637 let store = db.store();
1638 let mut header = store.physical_file_header().expect("physical header");
1639 assert_ne!(
1640 header.collection_roots_page, 0,
1641 "fixture retains native roots"
1642 );
1643 header.sequence = header
1644 .sequence
1645 .checked_add(1)
1646 .expect("next catalog sequence");
1647 header.manifest_page = 0;
1648 header.manifest_checksum = 0;
1649 header.registry_page = 0;
1650 header.registry_checksum = 0;
1651 header.recovery_page = 0;
1652 header.recovery_checksum = 0;
1653 header.catalog_page = 0;
1654 header.catalog_checksum = 0;
1655 header.metadata_state_page = 0;
1656 header.metadata_state_checksum = 0;
1657 header.vector_artifact_page = 0;
1658 header.vector_artifact_checksum = 0;
1659 store
1660 .update_physical_file_header(header)
1661 .expect("publish legacy catalog sequence");
1662
1663 let legacy = db.native_physical_state().expect("legacy native state");
1664 assert!(RedDB::native_state_is_summary_free_legacy(&legacy));
1665 let mut unreadable_publication = legacy.clone();
1666 unreadable_publication.header.registry_page = u32::MAX;
1667 assert!(
1668 !RedDB::native_state_is_summary_free_legacy(&unreadable_publication),
1669 "a non-zero unreadable summary pointer must still fail closed"
1670 );
1671
1672 let binary = PhysicalMetadataFile::metadata_binary_path_for(&path);
1673 let json = PhysicalMetadataFile::metadata_path_for(&path);
1674 db.delete_kv("red.storage.metadata", PHYSICAL_METADATA_KEY)
1675 .expect("legacy fixture");
1676 if binary.exists() {
1677 std::fs::remove_file(binary).expect("remove binary sidecar");
1678 }
1679 if json.exists() {
1680 std::fs::remove_file(json).expect("remove JSON sidecar");
1681 }
1682 for journal in PhysicalMetadataFile::journal_paths_for_data_path(&path)
1683 .expect("list metadata journals")
1684 {
1685 std::fs::remove_file(journal).expect("remove metadata journal");
1686 }
1687
1688 let metadata = db
1689 .load_or_bootstrap_physical_metadata(true)
1690 .expect("summary-free legacy state must bootstrap internal metadata");
1691 assert_eq!(
1692 metadata.superblock.collection_roots.get("retained"),
1693 db.physical_collection_roots().get("retained")
1694 );
1695 assert!(!PhysicalMetadataFile::metadata_binary_path_for(&path).exists());
1696 assert!(db.load_internal_physical_metadata().unwrap().is_some());
1697 let rows = runtime
1698 .execute_query("SELECT value FROM retained")
1699 .expect("retained row remains queryable");
1700 assert_eq!(rows.result.records.len(), 1);
1701 }
1702
1703 #[test]
1704 fn legacy_paged_drop_does_not_resurrect_derived_index_descriptors() {
1705 let directory = tempfile::tempdir().expect("temporary database");
1706 let path = directory.path().join("drop-descriptors.rdb");
1707 let options = RedDBOptions::persistent(&path)
1708 .with_memory_budget(64 * 1024 * 1024)
1709 .with_storage_profile(crate::StorageDeployPreset::PrimaryReplicaDev.selection())
1710 .expect("paged profile");
1711 {
1712 let runtime = crate::RedDBRuntime::with_options(options).expect("paged runtime");
1713 runtime
1714 .execute_query("CREATE TABLE retained_source (value INTEGER)")
1715 .expect("retained table");
1716 runtime
1717 .execute_query("INSERT INTO retained_source (value) VALUES (17)")
1718 .expect("retained row");
1719 runtime.checkpoint().expect("initial checkpoint");
1720 }
1721 let options = RedDBOptions::persistent(&path).with_memory_budget(64 * 1024 * 1024);
1724 {
1725 let runtime =
1726 crate::RedDBRuntime::with_options(options.clone()).expect("legacy reopen");
1727 assert!(runtime.db().is_paged());
1728 assert_eq!(
1729 runtime.db().options().storage_profile.deploy_profile,
1730 crate::storage::DeployProfile::Embedded
1731 );
1732 for cycle in 0..6 {
1734 let table = format!("discarded_source_{}", cycle % 4);
1735 runtime
1736 .execute_query(&format!("CREATE TABLE {table} (value INTEGER)"))
1737 .expect("temporary table");
1738 runtime.checkpoint().expect("publish derived descriptors");
1739 let before = runtime.db().physical_metadata().expect("physical metadata");
1740 assert!(before
1741 .indexes
1742 .iter()
1743 .any(|index| index.collection.as_deref() == Some(table.as_str())));
1744 runtime
1745 .execute_query(&format!("DROP TABLE {table}"))
1746 .expect("drop temporary table");
1747 for _ in 0..2 {
1748 runtime.checkpoint().expect("checkpoint after drop");
1749 let metadata = runtime.db().physical_metadata().expect("physical metadata");
1750 assert!(
1751 !metadata.indexes.iter().any(|index| index
1752 .collection
1753 .as_deref()
1754 .is_some_and(|name| name.starts_with("discarded_source_"))),
1755 "a dropped collection's derived descriptors were resurrected"
1756 );
1757 let native = runtime
1758 .db()
1759 .native_registry_summary()
1760 .expect("native registry");
1761 assert!(!native.indexes.iter().any(|index| {
1762 index
1763 .collection
1764 .as_deref()
1765 .is_some_and(|name| name.starts_with("discarded_source_"))
1766 }));
1767 }
1768 if cycle == 0 {
1769 let ghost = before
1772 .indexes
1773 .iter()
1774 .find(|index| index.collection.as_deref() == Some(table.as_str()))
1775 .expect("old derived descriptor")
1776 .clone();
1777 runtime
1778 .db()
1779 .update_physical_metadata(|metadata| {
1780 assert!(metadata.manifest_events.iter().any(|event| {
1781 event.collection == table
1782 && matches!(
1783 &event.kind,
1784 crate::physical::ManifestEventKind::Remove
1785 )
1786 }));
1787 metadata.indexes.push(ghost.clone());
1788 let mut orphan = ghost.clone();
1789 orphan.name = "audited_legacy_orphan".into();
1790 orphan.collection = Some("audited_missing_source".into());
1791 orphan.artifact_root_page = None;
1792 orphan.artifact_checksum = None;
1793 metadata.indexes.push(orphan.clone());
1794 orphan.name = "global_descriptor".into();
1795 orphan.collection = None;
1796 metadata.indexes.push(orphan);
1797 })
1798 .expect("simulate old metadata without deleting any rows");
1799 runtime
1800 .checkpoint()
1801 .expect("repair a confirmed old removal");
1802 }
1803 let metadata = runtime.db().physical_metadata().expect("physical metadata");
1804 assert!(metadata
1805 .indexes
1806 .iter()
1807 .any(|index| index.name == "audited_legacy_orphan"));
1808 assert!(metadata
1809 .indexes
1810 .iter()
1811 .any(|index| index.name == "global_descriptor"));
1812 }
1813 }
1814 let runtime = crate::RedDBRuntime::with_options(options).expect("second legacy reopen");
1815 assert_eq!(
1816 runtime
1817 .db()
1818 .store()
1819 .get_collection("retained_source")
1820 .expect("retained source")
1821 .count(),
1822 1
1823 );
1824 let metadata = runtime.db().physical_metadata().expect("physical metadata");
1825 assert!(!metadata.indexes.iter().any(|index| {
1826 index
1827 .collection
1828 .as_deref()
1829 .is_some_and(|name| name.starts_with("discarded_source_"))
1830 }));
1831 assert!(metadata
1832 .indexes
1833 .iter()
1834 .any(|index| index.collection.as_deref() == Some("retained_source")));
1835 assert!(metadata
1836 .indexes
1837 .iter()
1838 .any(|index| index.name == "audited_legacy_orphan"));
1839 assert!(metadata
1840 .indexes
1841 .iter()
1842 .any(|index| index.name == "global_descriptor"));
1843 }
1844
1845 #[test]
1846 fn index_reconciliation_reads_one_native_observation() {
1847 let directory = tempfile::tempdir().expect("temporary database");
1848 let runtime = crate::RedDBRuntime::with_options(
1849 RedDBOptions::persistent(directory.path().join("catalog.rdb"))
1850 .with_storage_profile(crate::StorageDeployPreset::Serverless.selection())
1851 .expect("paged profile")
1852 .with_capability(Capability::FullText),
1853 )
1854 .expect("runtime");
1855 for number in 0..4 {
1856 runtime
1857 .execute_query(&format!("CREATE TABLE source_{number} (payload TEXT)"))
1858 .expect("table");
1859 runtime
1860 .execute_query(&format!(
1861 "INSERT INTO source_{number} (payload) VALUES ('searchable payload')"
1862 ))
1863 .expect("row");
1864 }
1865 runtime.checkpoint().expect("published artifacts");
1866 let db = runtime.db();
1867 let pager = db.store.pager().expect("paged store");
1868 let reads = || {
1869 let stats = pager.cache_stats();
1870 stats.hits + stats.misses
1871 };
1872 let before = reads();
1873 let native = db.native_physical_state().expect("native state");
1874 let observation_reads = reads() - before;
1875 let artifacts = native.registry.expect("registry").vector_artifacts;
1876 let pages = native.vector_artifact_pages.expect("artifact pages");
1877 let indexes: Vec<_> = artifacts
1878 .iter()
1879 .filter(|artifact| artifact.artifact_kind == "text.fulltext")
1880 .map(|artifact| PhysicalIndexState {
1881 name: format!("{}::fulltext", artifact.collection),
1882 kind: IndexKind::FullText,
1883 collection: Some(artifact.collection.clone()),
1884 enabled: true,
1885 entries: 0,
1886 estimated_memory_bytes: 0,
1887 last_refresh_ms: None,
1888 backend: "catalog-derived".into(),
1889 artifact_kind: None,
1890 artifact_root_page: None,
1891 artifact_checksum: None,
1892 build_state: "catalog-derived".into(),
1893 })
1894 .collect();
1895 assert!(indexes.len() >= 4, "fixture must publish all four indexes");
1896 let declarations = indexes.clone();
1897 let before = reads();
1898 let indexes = db.reconcile_index_states_with_native_artifacts(indexes);
1899 let reconciliation_reads = reads() - before;
1900 for index in indexes {
1901 let artifact = artifacts
1902 .iter()
1903 .find(|artifact| {
1904 Some(&artifact.collection) == index.collection.as_ref()
1905 && artifact.artifact_kind == "text.fulltext"
1906 })
1907 .expect("published artifact");
1908 let page = pages
1909 .iter()
1910 .find(|page| {
1911 page.collection == artifact.collection
1912 && page.artifact_kind == artifact.artifact_kind
1913 })
1914 .expect("artifact root");
1915 assert_eq!(index.artifact_root_page, Some(page.root_page));
1916 assert_eq!(index.artifact_checksum, Some(artifact.checksum));
1917 assert_eq!(index.entries, artifact.vector_count as usize);
1918 assert_eq!(index.build_state, "artifact-published");
1919 }
1920 assert_eq!(
1921 reconciliation_reads, observation_reads,
1922 "catalog reconciliation must not reread artifact pages per index"
1923 );
1924
1925 let page_id = native.header.vector_artifact_page;
1928 let mut page = pager.read_page(page_id).expect("artifact directory");
1929 page.as_bytes_mut()[crate::storage::engine::HEADER_SIZE..].fill(0);
1930 pager
1931 .write_page(page_id, page)
1932 .expect("corrupt artifact directory");
1933 assert!(db.native_vector_artifact_pages().is_none());
1934 let indexes = db.reconcile_index_states_with_native_artifacts(declarations);
1935 for index in indexes {
1936 assert_eq!(index.artifact_root_page, None);
1937 assert!(
1938 index.artifact_checksum.is_some(),
1939 "registry remains available"
1940 );
1941 assert_eq!(index.build_state, "artifact-published");
1942 }
1943 }
1944}