Skip to main content

reddb_server/storage/unified/devx/reddb/
impl_metadata.rs

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