Skip to main content

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

1use super::*;
2
3impl RedDB {
4    pub(crate) fn write_column_block_page(
5        &self,
6        bytes: &[u8],
7    ) -> Result<crate::storage::engine::PageLocation, Box<dyn std::error::Error>> {
8        use crate::storage::engine::PageType;
9
10        let store = self.store();
11        let Some(pager) = store.pager() else {
12            return Ok(crate::storage::engine::PageLocation::new(
13                0,
14                0,
15                bytes.len() as u32,
16            ));
17        };
18        let content_size = crate::storage::engine::page::CONTENT_SIZE;
19        let page_count = bytes.len().max(1).div_ceil(content_size);
20        let mut first_page_id = None;
21        for page_index in 0..page_count {
22            let mut page = pager.allocate_page(PageType::ColumnBlock)?;
23            let page_id = page.page_id();
24            if let Some(first) = first_page_id {
25                let expected = first + page_index as u32;
26                if page_id != expected {
27                    return Err(format!(
28                        "column block page allocation was not contiguous: got {page_id}, expected {expected}"
29                    )
30                    .into());
31                }
32            } else {
33                first_page_id = Some(page_id);
34            }
35            let start = page_index * content_size;
36            let end = (start + content_size).min(bytes.len());
37            if start < end {
38                page.content_mut()[..end - start].copy_from_slice(&bytes[start..end]);
39            }
40            pager.write_page_encrypted(page_id, page)?;
41        }
42        Ok(crate::storage::engine::PageLocation::new(
43            first_page_id.expect("page_count is at least one"),
44            0,
45            bytes.len() as u32,
46        ))
47    }
48
49    pub(crate) fn read_column_block_page(
50        &self,
51        loc: crate::storage::engine::PageLocation,
52    ) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error>> {
53        let store = self.store();
54        let Some(pager) = store.pager() else {
55            return Ok(None);
56        };
57        let content_size = crate::storage::engine::page::CONTENT_SIZE;
58        let mut remaining = loc.length as usize;
59        let mut page_id = loc.page_id;
60        let mut offset = loc.offset as usize;
61        let mut out = Vec::with_capacity(remaining);
62        while remaining > 0 {
63            let page = pager.read_page_decrypted(page_id)?;
64            if page.page_type()? != crate::storage::engine::PageType::ColumnBlock {
65                return Err(format!("page {page_id} is not a ColumnBlock page").into());
66            }
67            if offset > content_size {
68                return Err(format!(
69                    "column block page location offset out of bounds: {offset} > {content_size}"
70                )
71                .into());
72            }
73            let available = content_size - offset;
74            let take = remaining.min(available);
75            out.extend_from_slice(&page.content()[offset..offset + take]);
76            remaining -= take;
77            page_id = page_id.saturating_add(1);
78            offset = 0;
79        }
80        Ok(Some(out))
81    }
82
83    pub fn tree_definitions(&self) -> Vec<crate::physical::PhysicalTreeDefinition> {
84        self.physical_metadata()
85            .map(|metadata| metadata.tree_definitions)
86            .unwrap_or_default()
87    }
88
89    pub fn tree_definition(
90        &self,
91        collection: &str,
92        name: &str,
93    ) -> Option<crate::physical::PhysicalTreeDefinition> {
94        self.tree_definitions()
95            .into_iter()
96            .find(|definition| definition.collection == collection && definition.name == name)
97    }
98
99    pub fn save_tree_definition(
100        &self,
101        definition: crate::physical::PhysicalTreeDefinition,
102    ) -> Result<crate::physical::PhysicalTreeDefinition, Box<dyn std::error::Error>> {
103        self.update_physical_metadata(|metadata| {
104            if let Some(existing) = metadata.tree_definitions.iter_mut().find(|existing| {
105                existing.collection == definition.collection && existing.name == definition.name
106            }) {
107                *existing = definition.clone();
108            } else {
109                metadata.tree_definitions.push(definition.clone());
110            }
111            metadata.tree_definitions.sort_by(|left, right| {
112                left.collection
113                    .cmp(&right.collection)
114                    .then_with(|| left.name.cmp(&right.name))
115            });
116            definition.clone()
117        })
118    }
119
120    pub fn remove_tree_definition(
121        &self,
122        collection: &str,
123        name: &str,
124    ) -> Result<Option<crate::physical::PhysicalTreeDefinition>, Box<dyn std::error::Error>> {
125        self.update_physical_metadata(|metadata| {
126            metadata
127                .tree_definitions
128                .iter()
129                .position(|definition| {
130                    definition.collection == collection && definition.name == name
131                })
132                .map(|index| metadata.tree_definitions.remove(index))
133        })
134    }
135
136    pub fn collection_default_ttl_ms(&self, collection: &str) -> Option<u64> {
137        self.collection_ttl_defaults_ms
138            .read()
139            .ok()
140            .and_then(|defaults| defaults.get(collection).copied())
141    }
142
143    pub fn set_collection_default_ttl_ms(&self, collection: impl Into<String>, ttl_ms: u64) {
144        if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
145            defaults.insert(collection.into(), ttl_ms);
146        }
147    }
148
149    pub fn clear_collection_default_ttl_ms(&self, collection: &str) {
150        if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
151            defaults.remove(collection);
152        }
153    }
154
155    pub fn collection_contracts(&self) -> Vec<crate::physical::CollectionContract> {
156        self.contract_cache_map()
157            .values()
158            .map(|arc| (**arc).clone())
159            .collect()
160    }
161
162    pub fn collection_contract(
163        &self,
164        collection: &str,
165    ) -> Option<crate::physical::CollectionContract> {
166        self.contract_cache_map()
167            .get(collection)
168            .map(|arc| (**arc).clone())
169    }
170
171    /// Arc-bumping variant — callers that only need read access skip
172    /// the per-call full `CollectionContract` clone. Deep clones cost
173    /// ~200-400 ns each (vec of declared columns + string allocs);
174    /// the UPDATE fast path hits this 2-3× per mutation.
175    pub fn collection_contract_arc(
176        &self,
177        collection: &str,
178    ) -> Option<std::sync::Arc<crate::physical::CollectionContract>> {
179        self.contract_cache_map()
180            .get(collection)
181            .map(std::sync::Arc::clone)
182    }
183
184    /// Return (or lazily build) the in-memory contract map.
185    /// Reparses PhysicalMetadataFile JSON only once per invalidation.
186    /// Called 3× per insert (ensure_model, enforce_uniqueness, normalize_fields)
187    /// so the cache is load-bearing for insert throughput.
188    fn contract_cache_map(
189        &self,
190    ) -> std::sync::Arc<
191        std::collections::HashMap<String, std::sync::Arc<crate::physical::CollectionContract>>,
192    > {
193        if let Ok(guard) = self.collection_contract_cache.read() {
194            if let Some(map) = guard.as_ref() {
195                return std::sync::Arc::clone(map);
196            }
197        }
198        let contracts: Vec<crate::physical::CollectionContract> = self
199            .physical_metadata()
200            .map(|metadata| metadata.collection_contracts)
201            .unwrap_or_default();
202        let map: std::collections::HashMap<_, _> = contracts
203            .into_iter()
204            .map(|contract| (contract.name.clone(), std::sync::Arc::new(contract)))
205            .collect();
206        let arc = std::sync::Arc::new(map);
207        if let Ok(mut guard) = self.collection_contract_cache.write() {
208            *guard = Some(std::sync::Arc::clone(&arc));
209        }
210        arc
211    }
212
213    pub(crate) fn invalidate_collection_contract_cache(&self) {
214        if let Ok(mut guard) = self.collection_contract_cache.write() {
215            *guard = None;
216        }
217    }
218
219    /// Single-file artifact only: mirror the current contract set into the
220    /// store's auxiliary-metadata blob so it rides the binary dump. Without
221    /// this the contracts live only in the in-memory cache and a collection's
222    /// declared model (e.g. `kv`) is lost on reopen — recovery would re-infer
223    /// it from the stored entities and a KV collection would come back as a
224    /// table. No-op for other profiles, which persist contracts via the
225    /// metadata sidecar.
226    pub(crate) fn sync_single_file_contract_blob(&self) {
227        let contracts = self.collection_contracts();
228        let bytes = crate::physical::serialize_collection_contracts(&contracts);
229        self.store().set_aux_metadata(bytes);
230    }
231
232    /// Seed the in-memory contract cache from the store's auxiliary-metadata
233    /// blob (single-file artifact). Called on open so a collection's declared
234    /// model survives a restart. Does nothing when the blob is absent (older
235    /// V9 dumps) or empty.
236    pub(crate) fn seed_contract_cache_from_store_aux(&self) {
237        let bytes = self.store().aux_metadata();
238        if bytes.is_empty() {
239            return;
240        }
241        let contracts = match crate::physical::deserialize_collection_contracts(&bytes) {
242            Ok(contracts) => contracts,
243            Err(_) => return,
244        };
245        let map: std::collections::HashMap<_, _> = contracts
246            .into_iter()
247            .map(|contract| (contract.name.clone(), std::sync::Arc::new(contract)))
248            .collect();
249        // Restore the per-collection state derived from contracts (context
250        // index enablement + default TTLs) so the full collection definition —
251        // not just the declared model — survives a restart.
252        let store = self.store();
253        let index = store.context_index();
254        if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
255            for contract in map.values() {
256                index.set_collection_enabled(&contract.name, contract.context_index_enabled);
257                if let Some(ttl_ms) = contract.default_ttl_ms {
258                    defaults.insert(contract.name.clone(), ttl_ms);
259                }
260            }
261        }
262        if let Ok(mut guard) = self.collection_contract_cache.write() {
263            *guard = Some(std::sync::Arc::new(map));
264        }
265    }
266
267    pub fn save_collection_contract(
268        &self,
269        contract: crate::physical::CollectionContract,
270    ) -> Result<crate::physical::CollectionContract, Box<dyn std::error::Error>> {
271        if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
272            if let Some(ttl_ms) = contract.default_ttl_ms {
273                defaults.insert(contract.name.clone(), ttl_ms);
274            } else {
275                defaults.remove(&contract.name);
276            }
277        }
278
279        self.store()
280            .context_index()
281            .set_collection_enabled(&contract.name, contract.context_index_enabled);
282        if let Some(manager) = self.store().get_collection(&contract.name) {
283            let columns = contract
284                .declared_columns
285                .iter()
286                .map(|column| column.name.clone())
287                .collect();
288            manager.set_column_schema_if_empty(columns);
289        }
290
291        if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
292            && self.options.storage_profile.packaging
293                == crate::storage::StoragePackaging::SingleFile
294        {
295            if let Ok(mut guard) = self.collection_contract_cache.write() {
296                let mut contracts = guard
297                    .as_ref()
298                    .map(|existing| existing.as_ref().clone())
299                    .unwrap_or_default();
300                contracts.insert(contract.name.clone(), std::sync::Arc::new(contract.clone()));
301                *guard = Some(std::sync::Arc::new(contracts));
302            }
303            self.sync_single_file_contract_blob();
304            return Ok(contract);
305        }
306
307        self.invalidate_collection_contract_cache();
308
309        let saved = self.update_physical_metadata(|metadata| {
310            if let Some(existing) = metadata
311                .collection_contracts
312                .iter_mut()
313                .find(|existing| existing.name == contract.name)
314            {
315                *existing = contract.clone();
316            } else {
317                metadata.collection_contracts.push(contract.clone());
318            }
319            metadata
320                .collection_contracts
321                .sort_by(|left, right| left.name.cmp(&right.name));
322
323            if let Some(ttl_ms) = contract.default_ttl_ms {
324                metadata
325                    .collection_ttl_defaults_ms
326                    .insert(contract.name.clone(), ttl_ms);
327            } else {
328                metadata.collection_ttl_defaults_ms.remove(&contract.name);
329            }
330
331            contract.clone()
332        })?;
333        self.invalidate_collection_contract_cache();
334        Ok(saved)
335    }
336
337    pub fn remove_collection_contract(
338        &self,
339        collection: &str,
340    ) -> Result<Option<crate::physical::CollectionContract>, Box<dyn std::error::Error>> {
341        if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
342            defaults.remove(collection);
343        }
344
345        self.store()
346            .context_index()
347            .set_collection_enabled(collection, false);
348
349        if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
350            && self.options.storage_profile.packaging
351                == crate::storage::StoragePackaging::SingleFile
352        {
353            let mut removed = None;
354            if let Ok(mut guard) = self.collection_contract_cache.write() {
355                let mut contracts = guard
356                    .as_ref()
357                    .map(|existing| existing.as_ref().clone())
358                    .unwrap_or_default();
359                removed = contracts
360                    .remove(collection)
361                    .map(|contract| (*contract).clone());
362                *guard = Some(std::sync::Arc::new(contracts));
363            }
364            self.sync_single_file_contract_blob();
365            return Ok(removed);
366        }
367
368        self.invalidate_collection_contract_cache();
369
370        let removed = self.update_physical_metadata(|metadata| {
371            let removed = metadata
372                .collection_contracts
373                .iter()
374                .position(|contract| contract.name == collection)
375                .map(|index| metadata.collection_contracts.remove(index));
376            metadata.collection_ttl_defaults_ms.remove(collection);
377            metadata
378                .indexes
379                .retain(|index| index.collection.as_deref() != Some(collection));
380            removed
381        })?;
382        self.invalidate_collection_contract_cache();
383        Ok(removed)
384    }
385
386    pub(crate) fn collection_ttl_defaults_snapshot(&self) -> BTreeMap<String, u64> {
387        self.collection_ttl_defaults_ms
388            .read()
389            .map(|defaults| {
390                defaults
391                    .iter()
392                    .map(|(collection, ttl_ms)| (collection.clone(), *ttl_ms))
393                    .collect()
394            })
395            .unwrap_or_default()
396    }
397
398    pub(crate) fn load_collection_ttl_defaults_from_metadata(&self) {
399        let metadata = self.physical_metadata();
400        let defaults = metadata
401            .as_ref()
402            .map(|m| m.collection_ttl_defaults_ms.clone())
403            .unwrap_or_default();
404
405        if let Ok(mut current) = self.collection_ttl_defaults_ms.write() {
406            current.clear();
407            current.extend(defaults);
408        }
409
410        if let Some(metadata) = metadata {
411            let store = self.store();
412            let index = store.context_index();
413            for contract in &metadata.collection_contracts {
414                index.set_collection_enabled(&contract.name, contract.context_index_enabled);
415            }
416        }
417    }
418
419    /// Snapshot the live hypertable registry (specs + every chunk)
420    /// into the sidecar-serialisable form. Called from
421    /// [`persist_metadata`] so the chunk routing spine is durable on
422    /// the same metadata path as collection contracts — issue #866.
423    /// Returns an empty vec (and touches nothing) when no hypertable
424    /// has ever been declared, so non-hypertable databases pay no
425    /// persist cost.
426    pub(crate) fn hypertable_registry_snapshot(&self) -> Vec<crate::physical::PhysicalHypertable> {
427        let registry = self.hypertables();
428        if registry.is_empty() {
429            return Vec::new();
430        }
431        use std::collections::BTreeMap;
432        let mut chunks_by_table: BTreeMap<String, Vec<crate::physical::PhysicalHypertableChunk>> =
433            BTreeMap::new();
434        for chunk in registry.snapshot_chunks() {
435            chunks_by_table
436                .entry(chunk.id.hypertable.clone())
437                .or_default()
438                .push(crate::physical::PhysicalHypertableChunk {
439                    start_ns: chunk.id.start_ns,
440                    end_ns_exclusive: chunk.end_ns_exclusive,
441                    row_count: chunk.row_count,
442                    min_ts_ns: chunk.min_ts_ns,
443                    max_ts_ns: chunk.max_ts_ns,
444                    sealed: chunk.sealed,
445                    ttl_override_ns: chunk.ttl_override_ns,
446                    columnar_page: chunk.columnar_page,
447                    columnar_derived: chunk.columnar_page.is_some(),
448                });
449        }
450        registry
451            .list()
452            .into_iter()
453            .map(|spec| crate::physical::PhysicalHypertable {
454                chunks: chunks_by_table.remove(&spec.name).unwrap_or_default(),
455                name: spec.name,
456                time_column: spec.time_column,
457                chunk_interval_ns: spec.chunk_interval_ns,
458                default_ttl_ns: spec.default_ttl_ns,
459            })
460            .collect()
461    }
462
463    /// Rehydrate the hypertable registry from the physical metadata
464    /// sidecar at boot. Mirror of
465    /// [`load_collection_ttl_defaults_from_metadata`] — specs and
466    /// chunks are restored verbatim so bounds / routing / TTL are
467    /// identical to the pre-restart state (issue #866).
468    pub(crate) fn load_hypertables_from_metadata(&self) {
469        let Some(metadata) = self.physical_metadata() else {
470            return;
471        };
472        if metadata.hypertables.is_empty() {
473            return;
474        }
475        let registry = self.hypertables();
476        for hypertable in &metadata.hypertables {
477            let mut spec = crate::storage::timeseries::HypertableSpec::new(
478                hypertable.name.clone(),
479                hypertable.time_column.clone(),
480                hypertable.chunk_interval_ns,
481            );
482            if let Some(ttl) = hypertable.default_ttl_ns {
483                spec = spec.with_ttl_ns(ttl);
484            }
485            registry.register(spec);
486            for chunk in &hypertable.chunks {
487                let meta = crate::storage::timeseries::ChunkMeta {
488                    id: crate::storage::timeseries::ChunkId {
489                        hypertable: hypertable.name.clone(),
490                        start_ns: chunk.start_ns,
491                    },
492                    end_ns_exclusive: chunk.end_ns_exclusive,
493                    row_count: chunk.row_count,
494                    min_ts_ns: chunk.min_ts_ns,
495                    max_ts_ns: chunk.max_ts_ns,
496                    sealed: chunk.sealed,
497                    ttl_override_ns: chunk.ttl_override_ns,
498                    columnar_page: chunk.columnar_page,
499                };
500                let id = meta.id.clone();
501                let columnar_page = meta.columnar_page;
502                registry.restore_chunk(meta);
503                if let Some(loc) = columnar_page {
504                    if let Ok(Some(bytes)) = self.read_column_block_page(loc) {
505                        registry.restore_columnar_block(&id, bytes);
506                    }
507                }
508            }
509        }
510    }
511
512    pub fn run_maintenance(&self) -> Result<(), Box<dyn std::error::Error>> {
513        self.store.run_maintenance()?;
514        self.persist_metadata()?;
515        Ok(())
516    }
517
518    /// Path to the physical metadata sidecar, if persistent.
519    pub fn metadata_path(&self) -> Option<PathBuf> {
520        self.path
521            .as_ref()
522            .map(|path| PhysicalMetadataFile::metadata_path_for(path))
523    }
524
525    /// Load the current physical metadata view, bootstrapping from native state when needed.
526    pub fn physical_metadata(&self) -> Option<PhysicalMetadataFile> {
527        self.load_or_bootstrap_physical_metadata(!self.options.read_only)
528            .ok()
529    }
530
531    /// Physical index registry derived for the current database state.
532    pub fn physical_indexes(&self) -> Vec<PhysicalIndexState> {
533        let indexes = self
534            .physical_metadata()
535            .map(|metadata| metadata.indexes)
536            .filter(|indexes| !indexes.is_empty())
537            .or_else(|| {
538                self.native_physical_state()
539                    .map(|state| self.physical_index_state_from_native_state(&state, None))
540            })
541            .unwrap_or_else(|| self.physical_index_state());
542        self.reconcile_index_states_with_native_artifacts(indexes)
543    }
544
545    /// List registered named exports from the current physical metadata view.
546    pub fn exports(&self) -> Vec<ExportDescriptor> {
547        self.physical_metadata()
548            .map(|metadata| metadata.exports)
549            .or_else(|| {
550                self.native_physical_state()
551                    .map(|state| self.exports_from_native_state(&state))
552            })
553            .unwrap_or_default()
554    }
555
556    /// List recorded snapshots from the current physical metadata view.
557    pub fn snapshots(&self) -> Vec<crate::physical::SnapshotDescriptor> {
558        if let Some(metadata) = self.physical_metadata() {
559            if !metadata.snapshots.is_empty() {
560                return metadata.snapshots;
561            }
562        }
563        self.native_physical_state()
564            .map(|state| self.snapshots_from_native_state(&state))
565            .unwrap_or_default()
566    }
567
568    /// List persisted named graph projections from the current physical metadata view.
569    pub fn graph_projections(&self) -> Vec<PhysicalGraphProjection> {
570        self.physical_metadata()
571            .map(|metadata| metadata.graph_projections)
572            .or_else(|| {
573                self.native_physical_state()
574                    .map(|state| self.graph_projections_from_native_state(&state))
575            })
576            .unwrap_or_default()
577    }
578
579    /// List graph projections declared in the catalog view.
580    pub fn declared_graph_projections(&self) -> Vec<PhysicalGraphProjection> {
581        self.catalog_model_snapshot().declared_graph_projections
582    }
583
584    /// List graph projections currently observed in the operational view.
585    pub fn operational_graph_projections(&self) -> Vec<PhysicalGraphProjection> {
586        self.graph_projections()
587            .into_iter()
588            .filter(|projection| {
589                projection.last_materialized_sequence.is_some()
590                    || matches!(projection.state.as_str(), "materialized" | "stale")
591            })
592            .collect()
593    }
594
595    /// List persisted analytics job metadata from the current physical metadata view.
596    pub fn analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
597        self.physical_metadata()
598            .map(|metadata| metadata.analytics_jobs)
599            .or_else(|| {
600                self.native_physical_state()
601                    .map(|state| self.analytics_jobs_from_native_state(&state))
602            })
603            .unwrap_or_default()
604    }
605
606    /// List analytics jobs declared in the catalog view.
607    pub fn declared_analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
608        self.catalog_model_snapshot().declared_analytics_jobs
609    }
610
611    /// List analytics jobs currently observed in the operational view.
612    pub fn operational_analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
613        self.analytics_jobs()
614            .into_iter()
615            .filter(|job| {
616                job.last_run_sequence.is_some()
617                    || matches!(
618                        job.state.as_str(),
619                        "running" | "completed" | "failed" | "queued" | "stale"
620                    )
621            })
622            .collect()
623    }
624
625    /// List indexes declared in the catalog view.
626    pub fn declared_indexes(&self) -> Vec<PhysicalIndexState> {
627        self.catalog_model_snapshot().declared_indexes
628    }
629
630    /// List indexes currently observed in the operational view.
631    pub fn operational_indexes(&self) -> Vec<PhysicalIndexState> {
632        self.catalog_model_snapshot().operational_indexes
633    }
634
635    /// List reconciled index status entries from the catalog snapshot.
636    pub fn index_statuses(&self) -> Vec<CatalogIndexStatus> {
637        self.catalog_model_snapshot().index_statuses
638    }
639
640    /// Resolve one index status entry from the catalog snapshot.
641    pub fn index_status(&self, name: &str) -> Option<CatalogIndexStatus> {
642        self.catalog_model_snapshot()
643            .index_statuses
644            .into_iter()
645            .find(|status| status.name == name)
646    }
647
648    /// Upsert a named graph projection in the persisted physical metadata.
649    pub fn save_graph_projection(
650        &self,
651        name: impl Into<String>,
652        node_labels: Vec<String>,
653        node_types: Vec<String>,
654        edge_labels: Vec<String>,
655        source: impl Into<String>,
656    ) -> Result<PhysicalGraphProjection, Box<dyn std::error::Error>> {
657        let name = name.into();
658        let source = source.into();
659        self.update_physical_metadata(|metadata| {
660            let now = SystemTime::now()
661                .duration_since(UNIX_EPOCH)
662                .unwrap_or_default()
663                .as_millis();
664            let projection = if let Some(existing) = metadata
665                .graph_projections
666                .iter_mut()
667                .find(|projection| projection.name == name)
668            {
669                existing.updated_at_unix_ms = now;
670                existing.state = "declared".to_string();
671                existing.source = source.clone();
672                existing.node_labels = node_labels.clone();
673                existing.node_types = node_types.clone();
674                existing.edge_labels = edge_labels.clone();
675                existing.last_materialized_sequence = None;
676                existing.clone()
677            } else {
678                let projection = PhysicalGraphProjection {
679                    name: name.clone(),
680                    created_at_unix_ms: now,
681                    updated_at_unix_ms: now,
682                    state: "declared".to_string(),
683                    source: source.clone(),
684                    node_labels: node_labels.clone(),
685                    node_types: node_types.clone(),
686                    edge_labels: edge_labels.clone(),
687                    last_materialized_sequence: None,
688                };
689                metadata.graph_projections.push(projection.clone());
690                projection
691            };
692
693            Self::mark_projection_dependent_jobs_stale(metadata, &name, now);
694
695            metadata
696                .graph_projections
697                .sort_by(|left, right| left.name.cmp(&right.name));
698            projection
699        })
700    }
701
702    /// Mark a declared graph projection as materialized in the current physical metadata.
703    pub fn materialize_graph_projection(
704        &self,
705        name: &str,
706    ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
707        self.update_physical_metadata(|metadata| {
708            let now = SystemTime::now()
709                .duration_since(UNIX_EPOCH)
710                .unwrap_or_default()
711                .as_millis();
712            let idx = metadata
713                .graph_projections
714                .iter()
715                .position(|projection| projection.name == name);
716            if let Some(idx) = idx {
717                metadata.graph_projections[idx].updated_at_unix_ms = now;
718                metadata.graph_projections[idx].state = "materialized".to_string();
719                metadata.graph_projections[idx].last_materialized_sequence =
720                    Some(metadata.superblock.sequence);
721                let result = metadata.graph_projections[idx].clone();
722                Self::rearm_projection_dependent_jobs_declared(metadata, name, now);
723                return Some(result);
724            }
725            None
726        })
727    }
728
729    /// Mark a declared graph projection as materializing.
730    pub fn mark_graph_projection_materializing(
731        &self,
732        name: &str,
733    ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
734        self.update_physical_metadata(|metadata| {
735            let now = SystemTime::now()
736                .duration_since(UNIX_EPOCH)
737                .unwrap_or_default()
738                .as_millis();
739            let idx = metadata
740                .graph_projections
741                .iter()
742                .position(|projection| projection.name == name);
743            if let Some(idx) = idx {
744                metadata.graph_projections[idx].updated_at_unix_ms = now;
745                metadata.graph_projections[idx].state = "materializing".to_string();
746                metadata.graph_projections[idx].last_materialized_sequence = None;
747                let result = metadata.graph_projections[idx].clone();
748                Self::mark_projection_dependent_jobs_stale(metadata, name, now);
749                return Some(result);
750            }
751            None
752        })
753    }
754
755    /// Mark a graph projection as failed.
756    pub fn fail_graph_projection(
757        &self,
758        name: &str,
759    ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
760        self.update_physical_metadata(|metadata| {
761            let now = SystemTime::now()
762                .duration_since(UNIX_EPOCH)
763                .unwrap_or_default()
764                .as_millis();
765            let idx = metadata
766                .graph_projections
767                .iter()
768                .position(|projection| projection.name == name);
769            if let Some(idx) = idx {
770                metadata.graph_projections[idx].updated_at_unix_ms = now;
771                metadata.graph_projections[idx].state = "failed".to_string();
772                metadata.graph_projections[idx].last_materialized_sequence = None;
773                let result = metadata.graph_projections[idx].clone();
774                Self::mark_projection_dependent_jobs_stale(metadata, name, now);
775                return Some(result);
776            }
777            None
778        })
779    }
780
781    /// Mark a graph projection as stale while preserving any last materialized sequence.
782    pub fn mark_graph_projection_stale(
783        &self,
784        name: &str,
785    ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
786        self.update_physical_metadata(|metadata| {
787            let now = SystemTime::now()
788                .duration_since(UNIX_EPOCH)
789                .unwrap_or_default()
790                .as_millis();
791            let idx = metadata
792                .graph_projections
793                .iter()
794                .position(|projection| projection.name == name);
795            if let Some(idx) = idx {
796                metadata.graph_projections[idx].updated_at_unix_ms = now;
797                metadata.graph_projections[idx].state = "stale".to_string();
798                let result = metadata.graph_projections[idx].clone();
799                Self::mark_projection_dependent_jobs_stale(metadata, name, now);
800                return Some(result);
801            }
802            None
803        })
804    }
805
806    fn mark_projection_dependent_jobs_stale(
807        metadata: &mut PhysicalMetadataFile,
808        projection_name: &str,
809        now: u128,
810    ) {
811        for job in metadata.analytics_jobs.iter_mut() {
812            if job.projection.as_deref() == Some(projection_name) && job.state != "declared" {
813                job.state = "stale".to_string();
814                job.updated_at_unix_ms = now;
815            }
816        }
817    }
818
819    fn rearm_projection_dependent_jobs_declared(
820        metadata: &mut PhysicalMetadataFile,
821        projection_name: &str,
822        now: u128,
823    ) {
824        for job in metadata.analytics_jobs.iter_mut() {
825            if job.projection.as_deref() == Some(projection_name) && job.state == "stale" {
826                job.state = "declared".to_string();
827                job.last_run_sequence = None;
828                job.updated_at_unix_ms = now;
829            }
830        }
831    }
832
833    /// Declare or update analytics job metadata without marking it as executed.
834    pub fn save_analytics_job(
835        &self,
836        kind: impl Into<String>,
837        projection: Option<String>,
838        metadata_entries: BTreeMap<String, String>,
839    ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
840        let kind = kind.into();
841        let job_id = Self::analytics_job_id(&kind, projection.as_deref());
842        if let Some(projection_name) = projection.as_deref() {
843            if !self.graph_projection_is_declared(projection_name) {
844                return Err(format!(
845                    "graph projection '{projection_name}' is not declared in physical metadata"
846                )
847                .into());
848            }
849        }
850
851        self.update_physical_metadata(|metadata| {
852            let now = SystemTime::now()
853                .duration_since(UNIX_EPOCH)
854                .unwrap_or_default()
855                .as_millis();
856
857            let job = if let Some(existing) = metadata
858                .analytics_jobs
859                .iter_mut()
860                .find(|job| job.id == job_id)
861            {
862                existing.kind = kind.clone();
863                existing.projection = projection.clone();
864                existing.updated_at_unix_ms = now;
865                existing.metadata = metadata_entries.clone();
866                if existing.last_run_sequence.is_none() {
867                    existing.state = "declared".to_string();
868                }
869                existing.clone()
870            } else {
871                let job = PhysicalAnalyticsJob {
872                    id: job_id.clone(),
873                    kind: kind.clone(),
874                    state: "declared".to_string(),
875                    projection: projection.clone(),
876                    created_at_unix_ms: now,
877                    updated_at_unix_ms: now,
878                    last_run_sequence: None,
879                    metadata: metadata_entries.clone(),
880                };
881                metadata.analytics_jobs.push(job.clone());
882                job
883            };
884
885            metadata
886                .analytics_jobs
887                .sort_by(|left, right| left.id.cmp(&right.id));
888            job
889        })
890    }
891
892    /// Record or update analytics job metadata in the persisted physical metadata.
893    pub fn record_analytics_job(
894        &self,
895        kind: impl Into<String>,
896        projection: Option<String>,
897        metadata_entries: BTreeMap<String, String>,
898    ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
899        let kind = kind.into();
900        let job_id = Self::analytics_job_id(&kind, projection.as_deref());
901        if let Some(projection_name) = projection.as_deref() {
902            if !self.graph_projection_is_declared(projection_name) {
903                return Err(format!(
904                    "graph projection '{projection_name}' is not declared in physical metadata"
905                )
906                .into());
907            }
908            if !self.graph_projection_is_operational(projection_name) {
909                return Err(format!(
910                    "graph projection '{projection_name}' is declared but not operationally materialized"
911                )
912                .into());
913            }
914        }
915
916        self.update_physical_metadata(|metadata| {
917            let now = SystemTime::now()
918                .duration_since(UNIX_EPOCH)
919                .unwrap_or_default()
920                .as_millis();
921
922            let existing = metadata
923                .analytics_jobs
924                .iter_mut()
925                .find(|job| job.id == job_id)?;
926            existing.state = "completed".to_string();
927            existing.kind = kind.clone();
928            existing.projection = projection.clone();
929            existing.updated_at_unix_ms = now;
930            existing.last_run_sequence = Some(metadata.superblock.sequence);
931            existing.metadata = metadata_entries.clone();
932            let job = existing.clone();
933
934            metadata
935                .analytics_jobs
936                .sort_by(|left, right| left.id.cmp(&right.id));
937            Some(job)
938        })
939        .and_then(|job| {
940            job.ok_or_else(|| {
941                format!("analytics job '{job_id}' is not declared in physical metadata").into()
942            })
943        })
944    }
945
946    /// Mark a declared analytics job as running.
947    pub fn queue_analytics_job(
948        &self,
949        kind: impl Into<String>,
950        projection: Option<String>,
951        metadata_entries: BTreeMap<String, String>,
952    ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
953        let kind = kind.into();
954        let job_id = Self::analytics_job_id(&kind, projection.as_deref());
955        if let Some(projection_name) = projection.as_deref() {
956            if !self.graph_projection_is_declared(projection_name) {
957                return Err(format!(
958                    "graph projection '{projection_name}' is not declared in physical metadata"
959                )
960                .into());
961            }
962            if !self.graph_projection_is_operational(projection_name) {
963                return Err(format!(
964                    "graph projection '{projection_name}' is declared but not operationally materialized"
965                )
966                .into());
967            }
968        }
969
970        self.update_physical_metadata(|metadata| {
971            let now = SystemTime::now()
972                .duration_since(UNIX_EPOCH)
973                .unwrap_or_default()
974                .as_millis();
975
976            let existing = metadata
977                .analytics_jobs
978                .iter_mut()
979                .find(|job| job.id == job_id)?;
980            existing.state = "queued".to_string();
981            existing.kind = kind.clone();
982            existing.projection = projection.clone();
983            existing.updated_at_unix_ms = now;
984            existing.metadata = metadata_entries.clone();
985            let job = existing.clone();
986
987            metadata
988                .analytics_jobs
989                .sort_by(|left, right| left.id.cmp(&right.id));
990            Some(job)
991        })
992        .and_then(|job| {
993            job.ok_or_else(|| {
994                format!("analytics job '{job_id}' is not declared in physical metadata").into()
995            })
996        })
997    }
998
999    /// Mark a declared analytics job as running.
1000    pub fn start_analytics_job(
1001        &self,
1002        kind: impl Into<String>,
1003        projection: Option<String>,
1004        metadata_entries: BTreeMap<String, String>,
1005    ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1006        let kind = kind.into();
1007        let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1008        if let Some(projection_name) = projection.as_deref() {
1009            if !self.graph_projection_is_declared(projection_name) {
1010                return Err(format!(
1011                    "graph projection '{projection_name}' is not declared in physical metadata"
1012                )
1013                .into());
1014            }
1015            if !self.graph_projection_is_operational(projection_name) {
1016                return Err(format!(
1017                    "graph projection '{projection_name}' is declared but not operationally materialized"
1018                )
1019                .into());
1020            }
1021        }
1022
1023        self.update_physical_metadata(|metadata| {
1024            let now = SystemTime::now()
1025                .duration_since(UNIX_EPOCH)
1026                .unwrap_or_default()
1027                .as_millis();
1028
1029            let existing = metadata
1030                .analytics_jobs
1031                .iter_mut()
1032                .find(|job| job.id == job_id)?;
1033            existing.state = "running".to_string();
1034            existing.kind = kind.clone();
1035            existing.projection = projection.clone();
1036            existing.updated_at_unix_ms = now;
1037            existing.metadata = metadata_entries.clone();
1038            let job = existing.clone();
1039
1040            metadata
1041                .analytics_jobs
1042                .sort_by(|left, right| left.id.cmp(&right.id));
1043            Some(job)
1044        })
1045        .and_then(|job| {
1046            job.ok_or_else(|| {
1047                format!("analytics job '{job_id}' is not declared in physical metadata").into()
1048            })
1049        })
1050    }
1051
1052    /// Mark a declared analytics job as failed.
1053    pub fn fail_analytics_job(
1054        &self,
1055        kind: impl Into<String>,
1056        projection: Option<String>,
1057        metadata_entries: BTreeMap<String, String>,
1058    ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1059        let kind = kind.into();
1060        let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1061
1062        self.update_physical_metadata(|metadata| {
1063            let now = SystemTime::now()
1064                .duration_since(UNIX_EPOCH)
1065                .unwrap_or_default()
1066                .as_millis();
1067
1068            let existing = metadata
1069                .analytics_jobs
1070                .iter_mut()
1071                .find(|job| job.id == job_id)?;
1072            existing.state = "failed".to_string();
1073            existing.kind = kind.clone();
1074            existing.projection = projection.clone();
1075            existing.updated_at_unix_ms = now;
1076            existing.metadata = metadata_entries.clone();
1077            let job = existing.clone();
1078
1079            metadata
1080                .analytics_jobs
1081                .sort_by(|left, right| left.id.cmp(&right.id));
1082            Some(job)
1083        })
1084        .and_then(|job| {
1085            job.ok_or_else(|| {
1086                format!("analytics job '{job_id}' is not declared in physical metadata").into()
1087            })
1088        })
1089    }
1090
1091    /// Mark a declared analytics job as stale.
1092    pub fn mark_analytics_job_stale(
1093        &self,
1094        kind: impl Into<String>,
1095        projection: Option<String>,
1096        metadata_entries: BTreeMap<String, String>,
1097    ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1098        let kind = kind.into();
1099        let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1100
1101        self.update_physical_metadata(|metadata| {
1102            let now = SystemTime::now()
1103                .duration_since(UNIX_EPOCH)
1104                .unwrap_or_default()
1105                .as_millis();
1106
1107            let existing = metadata
1108                .analytics_jobs
1109                .iter_mut()
1110                .find(|job| job.id == job_id)?;
1111            existing.state = "stale".to_string();
1112            existing.kind = kind.clone();
1113            existing.projection = projection.clone();
1114            existing.updated_at_unix_ms = now;
1115            existing.metadata = metadata_entries.clone();
1116            let job = existing.clone();
1117
1118            metadata
1119                .analytics_jobs
1120                .sort_by(|left, right| left.id.cmp(&right.id));
1121            Some(job)
1122        })
1123        .and_then(|job| {
1124            job.ok_or_else(|| {
1125                format!("analytics job '{job_id}' is not declared in physical metadata").into()
1126            })
1127        })
1128    }
1129
1130    /// Create a named export by copying the current database file and metadata sidecar.
1131    pub fn create_named_export(
1132        &self,
1133        name: impl Into<String>,
1134    ) -> Result<ExportDescriptor, Box<dyn std::error::Error>> {
1135        let name = name.into();
1136        if self.options.mode != StorageMode::Persistent {
1137            return Err("exports require persistent mode".into());
1138        }
1139        let Some(path) = self.path() else {
1140            return Err("database path is not available".into());
1141        };
1142
1143        self.flush()?;
1144
1145        let mut metadata = match self.load_or_bootstrap_physical_metadata(true) {
1146            Ok(metadata) => metadata,
1147            Err(_)
1148                if self.options.storage_profile.deploy_profile
1149                    == crate::storage::DeployProfile::Embedded
1150                    && self.options.storage_profile.packaging
1151                        == crate::storage::StoragePackaging::SingleFile =>
1152            {
1153                crate::physical::PhysicalMetadataFile::from_state(
1154                    self.options.clone(),
1155                    self.catalog_snapshot(),
1156                    self.physical_collection_roots(),
1157                    self.physical_index_state(),
1158                    None,
1159                )
1160            }
1161            Err(err) => return Err(err),
1162        };
1163        let export_data_path = reddb_file::copy_physical_export_data_file(path, &name)?;
1164        let export_metadata_path = PhysicalMetadataFile::metadata_path_for(&export_data_path);
1165        let export_metadata_binary_path =
1166            PhysicalMetadataFile::metadata_binary_path_for(&export_data_path);
1167
1168        let descriptor = ExportDescriptor {
1169            name: name.clone(),
1170            created_at_unix_ms: SystemTime::now()
1171                .duration_since(UNIX_EPOCH)
1172                .unwrap_or_default()
1173                .as_millis(),
1174            snapshot_id: metadata
1175                .snapshots
1176                .last()
1177                .map(|snapshot| snapshot.snapshot_id),
1178            superblock_sequence: metadata.superblock.sequence,
1179            data_path: export_data_path.display().to_string(),
1180            metadata_path: export_metadata_path.display().to_string(),
1181            collection_count: metadata.catalog.total_collections,
1182            total_entities: metadata.catalog.total_entities,
1183        };
1184
1185        metadata
1186            .exports
1187            .retain(|export| export.name != descriptor.name);
1188        metadata.exports.push(descriptor.clone());
1189        self.prune_export_registry(&mut metadata.exports);
1190        metadata.save_for_data_path(path)?;
1191        metadata.save_to_binary_path(&export_metadata_binary_path)?;
1192        metadata.save_to_path(&export_metadata_path)?;
1193
1194        Ok(descriptor)
1195    }
1196
1197    /// Enable or disable a physical index entry in the persisted registry.
1198    pub fn set_index_enabled(
1199        &self,
1200        name: &str,
1201        enabled: bool,
1202    ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1203        let Some(status) = self.index_status(name) else {
1204            return Err(format!("index '{name}' is not present in catalog status").into());
1205        };
1206        if !status.declared {
1207            return Err(format!("index '{name}' is not declared in physical metadata").into());
1208        }
1209        self.update_physical_metadata(|metadata| {
1210            let now = SystemTime::now()
1211                .duration_since(UNIX_EPOCH)
1212                .unwrap_or_default()
1213                .as_millis();
1214            if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1215                index.enabled = enabled;
1216                if !enabled {
1217                    index.build_state = "disabled".to_string();
1218                } else if index.build_state == "disabled" {
1219                    index.build_state = if index.artifact_root_page.is_some() {
1220                        "ready".to_string()
1221                    } else {
1222                        "declared-unbuilt".to_string()
1223                    };
1224                }
1225                index.last_refresh_ms = Some(now);
1226                return Some(index.clone());
1227            }
1228            None
1229        })
1230    }
1231
1232    /// Mark a declared physical index as building in the persisted registry.
1233    pub fn mark_index_building(
1234        &self,
1235        name: &str,
1236    ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1237        let Some(status) = self.index_status(name) else {
1238            return Err(format!("index '{name}' is not present in catalog status").into());
1239        };
1240        if !status.declared {
1241            return Err(format!("index '{name}' is not declared in physical metadata").into());
1242        }
1243        if status.lifecycle_state == "disabled" {
1244            return Err(format!("index '{name}' is disabled").into());
1245        }
1246        self.update_physical_metadata(|metadata| {
1247            let now = SystemTime::now()
1248                .duration_since(UNIX_EPOCH)
1249                .unwrap_or_default()
1250                .as_millis();
1251            if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1252                index.build_state = "building".to_string();
1253                index.last_refresh_ms = Some(now);
1254                return Some(index.clone());
1255            }
1256            None
1257        })
1258    }
1259
1260    /// Mark a declared physical index as failed in the persisted registry.
1261    pub fn fail_index(
1262        &self,
1263        name: &str,
1264    ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1265        let Some(status) = self.index_status(name) else {
1266            return Err(format!("index '{name}' is not present in catalog status").into());
1267        };
1268        if !status.declared {
1269            return Err(format!("index '{name}' is not declared in physical metadata").into());
1270        }
1271        self.update_physical_metadata(|metadata| {
1272            let now = SystemTime::now()
1273                .duration_since(UNIX_EPOCH)
1274                .unwrap_or_default()
1275                .as_millis();
1276            if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1277                index.build_state = "failed".to_string();
1278                index.last_refresh_ms = Some(now);
1279                return Some(index.clone());
1280            }
1281            None
1282        })
1283    }
1284
1285    /// Mark a declared physical index as stale in the persisted registry.
1286    pub fn mark_index_stale(
1287        &self,
1288        name: &str,
1289    ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1290        let Some(status) = self.index_status(name) else {
1291            return Err(format!("index '{name}' is not present in catalog status").into());
1292        };
1293        if !status.declared {
1294            return Err(format!("index '{name}' is not declared in physical metadata").into());
1295        }
1296        self.update_physical_metadata(|metadata| {
1297            let now = SystemTime::now()
1298                .duration_since(UNIX_EPOCH)
1299                .unwrap_or_default()
1300                .as_millis();
1301            if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1302                index.build_state = if index.enabled {
1303                    "stale".to_string()
1304                } else {
1305                    "disabled".to_string()
1306                };
1307                index.last_refresh_ms = Some(now);
1308                return Some(index.clone());
1309            }
1310            None
1311        })
1312    }
1313
1314    /// Mark a declared physical index as ready in the persisted registry.
1315    pub fn mark_index_ready(
1316        &self,
1317        name: &str,
1318    ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1319        self.warmup_index(name)
1320    }
1321
1322    /// Mark a physical index as warmed up/refreshed in the persisted registry.
1323    pub fn warmup_index(
1324        &self,
1325        name: &str,
1326    ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1327        let Some(status) = self.index_status(name) else {
1328            return Err(format!("index '{name}' is not present in catalog status").into());
1329        };
1330        if !status.declared {
1331            return Err(format!("index '{name}' is not declared in physical metadata").into());
1332        }
1333        if status.lifecycle_state == "disabled" {
1334            return Err(format!("index '{name}' is disabled").into());
1335        }
1336        if !status.operational {
1337            return Err(
1338                format!("index '{name}' is declared but not operationally materialized").into(),
1339            );
1340        }
1341        let warmed_artifact = self
1342            .physical_indexes()
1343            .into_iter()
1344            .find(|index| index.name == name)
1345            .map(|mut index| {
1346                self.warmup_native_vector_artifact_for_index(&index)?;
1347                self.apply_runtime_native_artifact_to_index_state(&mut index)?;
1348                Ok::<_, String>(index)
1349            })
1350            .transpose()
1351            .map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
1352
1353        self.update_physical_metadata(|metadata| {
1354            let now = SystemTime::now()
1355                .duration_since(UNIX_EPOCH)
1356                .unwrap_or_default()
1357                .as_millis();
1358            if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1359                if let Some(warmed) = warmed_artifact.as_ref() {
1360                    index.entries = warmed.entries;
1361                    index.estimated_memory_bytes = warmed.estimated_memory_bytes;
1362                    index.backend = warmed.backend.clone();
1363                    index.build_state = "ready".to_string();
1364                }
1365                index.last_refresh_ms = Some(now);
1366                return Some(index.clone());
1367            }
1368            None
1369        })
1370    }
1371
1372    /// Rebuild physical index metadata from the current catalog, optionally restricted to one collection.
1373    pub fn rebuild_index_registry(
1374        &self,
1375        collection: Option<&str>,
1376    ) -> Result<Vec<PhysicalIndexState>, Box<dyn std::error::Error>> {
1377        let fresh = self.reconcile_index_states_with_native_artifacts(self.physical_index_state());
1378        self.update_physical_metadata(|metadata| {
1379            let now = SystemTime::now()
1380                .duration_since(UNIX_EPOCH)
1381                .unwrap_or_default()
1382                .as_millis();
1383
1384            let mut affected = Vec::new();
1385            let declared = metadata.indexes.clone();
1386            for declared_index in declared {
1387                let matches_collection = collection.is_none_or(|collection_name| {
1388                    declared_index.collection.as_deref() == Some(collection_name)
1389                });
1390                if !matches_collection {
1391                    continue;
1392                }
1393
1394                let mut rebuilt = fresh
1395                    .iter()
1396                    .find(|index| index.name == declared_index.name)
1397                    .cloned()
1398                    .unwrap_or_else(|| {
1399                        let mut index = declared_index.clone();
1400                        index.build_state = "declared-unbuilt".to_string();
1401                        index
1402                    });
1403                rebuilt.enabled = declared_index.enabled;
1404                rebuilt.artifact_kind = rebuilt
1405                    .artifact_kind
1406                    .or_else(|| declared_index.artifact_kind.clone());
1407                rebuilt.artifact_root_page = rebuilt
1408                    .artifact_root_page
1409                    .or(declared_index.artifact_root_page);
1410                rebuilt.artifact_checksum = rebuilt
1411                    .artifact_checksum
1412                    .or(declared_index.artifact_checksum);
1413                rebuilt.build_state =
1414                    Self::finalize_rebuilt_index_build_state(&declared_index, &rebuilt);
1415                rebuilt.last_refresh_ms = Some(now);
1416
1417                if let Some(existing) = metadata
1418                    .indexes
1419                    .iter_mut()
1420                    .find(|index| index.name == rebuilt.name)
1421                {
1422                    *existing = rebuilt.clone();
1423                } else {
1424                    metadata.indexes.push(rebuilt.clone());
1425                }
1426
1427                affected.push(rebuilt);
1428            }
1429
1430            affected
1431        })
1432    }
1433
1434    fn finalize_rebuilt_index_build_state(
1435        declared: &PhysicalIndexState,
1436        rebuilt: &PhysicalIndexState,
1437    ) -> String {
1438        if !rebuilt.enabled {
1439            return "disabled".to_string();
1440        }
1441
1442        if declared.build_state == "failed" || rebuilt.build_state == "failed" {
1443            return "failed".to_string();
1444        }
1445
1446        let native_artifact_family = Self::native_artifact_kind_for_index(rebuilt.kind).is_some();
1447        if native_artifact_family {
1448            if rebuilt.artifact_root_page.is_some() && rebuilt.artifact_checksum.is_some() {
1449                return "ready".to_string();
1450            }
1451            if declared.artifact_root_page.is_some()
1452                || declared.artifact_checksum.is_some()
1453                || declared.artifact_kind.is_some()
1454            {
1455                return "stale".to_string();
1456            }
1457            return "declared-unbuilt".to_string();
1458        }
1459
1460        if rebuilt.entries > 0 {
1461            return "ready".to_string();
1462        }
1463
1464        if matches!(
1465            declared.build_state.as_str(),
1466            "stale" | "artifact-published" | "registry-loaded"
1467        ) {
1468            return "stale".to_string();
1469        }
1470
1471        "declared-unbuilt".to_string()
1472    }
1473}