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 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 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 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 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 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 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 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 pub fn metadata_path(&self) -> Option<PathBuf> {
520 self.path
521 .as_ref()
522 .map(|path| PhysicalMetadataFile::metadata_path_for(path))
523 }
524
525 pub fn physical_metadata(&self) -> Option<PhysicalMetadataFile> {
527 self.load_or_bootstrap_physical_metadata(!self.options.read_only)
528 .ok()
529 }
530
531 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 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 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 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 pub fn declared_graph_projections(&self) -> Vec<PhysicalGraphProjection> {
581 self.catalog_model_snapshot().declared_graph_projections
582 }
583
584 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 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 pub fn declared_analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
608 self.catalog_model_snapshot().declared_analytics_jobs
609 }
610
611 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 pub fn declared_indexes(&self) -> Vec<PhysicalIndexState> {
627 self.catalog_model_snapshot().declared_indexes
628 }
629
630 pub fn operational_indexes(&self) -> Vec<PhysicalIndexState> {
632 self.catalog_model_snapshot().operational_indexes
633 }
634
635 pub fn index_statuses(&self) -> Vec<CatalogIndexStatus> {
637 self.catalog_model_snapshot().index_statuses
638 }
639
640 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}