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) -> std::io::Result<()> {
237 let bytes = self.store().aux_metadata();
238 if bytes.is_empty() {
239 return Ok(());
240 }
241 let contracts = crate::physical::deserialize_collection_contracts(&bytes)?;
242 let map: std::collections::HashMap<_, _> = contracts
243 .into_iter()
244 .map(|contract| (contract.name.clone(), std::sync::Arc::new(contract)))
245 .collect();
246 let store = self.store();
250 let index = store.context_index();
251 if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
252 for contract in map.values() {
253 index.set_collection_enabled(&contract.name, contract.context_index_enabled);
254 if let Some(ttl_ms) = contract.default_ttl_ms {
255 defaults.insert(contract.name.clone(), ttl_ms);
256 }
257 }
258 }
259 if let Ok(mut guard) = self.collection_contract_cache.write() {
260 *guard = Some(std::sync::Arc::new(map));
261 }
262 Ok(())
263 }
264
265 pub fn save_collection_contract(
266 &self,
267 contract: crate::physical::CollectionContract,
268 ) -> Result<crate::physical::CollectionContract, Box<dyn std::error::Error>> {
269 if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
270 if let Some(ttl_ms) = contract.default_ttl_ms {
271 defaults.insert(contract.name.clone(), ttl_ms);
272 } else {
273 defaults.remove(&contract.name);
274 }
275 }
276
277 self.store()
278 .context_index()
279 .set_collection_enabled(&contract.name, contract.context_index_enabled);
280 if let Some(manager) = self.store().get_collection(&contract.name) {
281 let columns = contract
282 .declared_columns
283 .iter()
284 .map(|column| column.name.clone())
285 .collect();
286 manager.set_column_schema_if_empty(columns);
287 }
288
289 if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
290 && self.options.storage_profile.packaging
291 == crate::storage::StoragePackaging::SingleFile
292 {
293 if let Ok(mut guard) = self.collection_contract_cache.write() {
294 let mut contracts = guard
295 .as_ref()
296 .map(|existing| existing.as_ref().clone())
297 .unwrap_or_default();
298 contracts.insert(contract.name.clone(), std::sync::Arc::new(contract.clone()));
299 *guard = Some(std::sync::Arc::new(contracts));
300 }
301 self.sync_single_file_contract_blob();
302 return Ok(contract);
303 }
304
305 self.invalidate_collection_contract_cache();
306
307 let saved = self.update_physical_metadata(|metadata| {
308 if let Some(existing) = metadata
309 .collection_contracts
310 .iter_mut()
311 .find(|existing| existing.name == contract.name)
312 {
313 *existing = contract.clone();
314 } else {
315 metadata.collection_contracts.push(contract.clone());
316 }
317 metadata
318 .collection_contracts
319 .sort_by(|left, right| left.name.cmp(&right.name));
320
321 if let Some(ttl_ms) = contract.default_ttl_ms {
322 metadata
323 .collection_ttl_defaults_ms
324 .insert(contract.name.clone(), ttl_ms);
325 } else {
326 metadata.collection_ttl_defaults_ms.remove(&contract.name);
327 }
328
329 contract.clone()
330 })?;
331 self.invalidate_collection_contract_cache();
332 Ok(saved)
333 }
334
335 pub fn remove_collection_contract(
336 &self,
337 collection: &str,
338 ) -> Result<Option<crate::physical::CollectionContract>, Box<dyn std::error::Error>> {
339 if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
340 defaults.remove(collection);
341 }
342
343 self.store()
344 .context_index()
345 .set_collection_enabled(collection, false);
346
347 if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
348 && self.options.storage_profile.packaging
349 == crate::storage::StoragePackaging::SingleFile
350 {
351 let mut removed = None;
352 if let Ok(mut guard) = self.collection_contract_cache.write() {
353 let mut contracts = guard
354 .as_ref()
355 .map(|existing| existing.as_ref().clone())
356 .unwrap_or_default();
357 removed = contracts
358 .remove(collection)
359 .map(|contract| (*contract).clone());
360 *guard = Some(std::sync::Arc::new(contracts));
361 }
362 self.sync_single_file_contract_blob();
363 return Ok(removed);
364 }
365
366 self.invalidate_collection_contract_cache();
367
368 let removed = self.update_physical_metadata(|metadata| {
369 let removed = metadata
370 .collection_contracts
371 .iter()
372 .position(|contract| contract.name == collection)
373 .map(|index| metadata.collection_contracts.remove(index));
374 metadata.collection_ttl_defaults_ms.remove(collection);
375 metadata
376 .indexes
377 .retain(|index| index.collection.as_deref() != Some(collection));
378 removed
379 })?;
380 self.invalidate_collection_contract_cache();
381 Ok(removed)
382 }
383
384 pub(crate) fn collection_ttl_defaults_snapshot(&self) -> BTreeMap<String, u64> {
385 self.collection_ttl_defaults_ms
386 .read()
387 .map(|defaults| {
388 defaults
389 .iter()
390 .map(|(collection, ttl_ms)| (collection.clone(), *ttl_ms))
391 .collect()
392 })
393 .unwrap_or_default()
394 }
395
396 pub(crate) fn load_collection_ttl_defaults_from_metadata(&self) {
397 let metadata = self.physical_metadata();
398 let defaults = metadata
399 .as_ref()
400 .map(|m| m.collection_ttl_defaults_ms.clone())
401 .unwrap_or_default();
402
403 if let Ok(mut current) = self.collection_ttl_defaults_ms.write() {
404 current.clear();
405 current.extend(defaults);
406 }
407
408 if let Some(metadata) = metadata {
409 let store = self.store();
410 let index = store.context_index();
411 for contract in &metadata.collection_contracts {
412 index.set_collection_enabled(&contract.name, contract.context_index_enabled);
413 }
414 }
415 }
416
417 pub(crate) fn hypertable_registry_snapshot(&self) -> Vec<crate::physical::PhysicalHypertable> {
425 let registry = self.hypertables();
426 if registry.is_empty() {
427 return Vec::new();
428 }
429 use std::collections::BTreeMap;
430 let mut chunks_by_table: BTreeMap<String, Vec<crate::physical::PhysicalHypertableChunk>> =
431 BTreeMap::new();
432 for chunk in registry.snapshot_chunks() {
433 chunks_by_table
434 .entry(chunk.id.hypertable.clone())
435 .or_default()
436 .push(crate::physical::PhysicalHypertableChunk {
437 start_ns: chunk.id.start_ns,
438 end_ns_exclusive: chunk.end_ns_exclusive,
439 row_count: chunk.row_count,
440 min_ts_ns: chunk.min_ts_ns,
441 max_ts_ns: chunk.max_ts_ns,
442 sealed: chunk.sealed,
443 ttl_override_ns: chunk.ttl_override_ns,
444 columnar_page: chunk.columnar_page,
445 columnar_derived: chunk.columnar_page.is_some(),
446 });
447 }
448 registry
449 .list()
450 .into_iter()
451 .map(|spec| crate::physical::PhysicalHypertable {
452 chunks: chunks_by_table.remove(&spec.name).unwrap_or_default(),
453 name: spec.name,
454 time_column: spec.time_column,
455 chunk_interval_ns: spec.chunk_interval_ns,
456 default_ttl_ns: spec.default_ttl_ns,
457 })
458 .collect()
459 }
460
461 pub(crate) fn load_hypertables_from_metadata(&self) {
467 let Some(metadata) = self.physical_metadata() else {
468 return;
469 };
470 if metadata.hypertables.is_empty() {
471 return;
472 }
473 let registry = self.hypertables();
474 for hypertable in &metadata.hypertables {
475 let mut spec = crate::storage::timeseries::HypertableSpec::new(
476 hypertable.name.clone(),
477 hypertable.time_column.clone(),
478 hypertable.chunk_interval_ns,
479 );
480 if let Some(ttl) = hypertable.default_ttl_ns {
481 spec = spec.with_ttl_ns(ttl);
482 }
483 registry.register(spec);
484 for chunk in &hypertable.chunks {
485 let meta = crate::storage::timeseries::ChunkMeta {
486 id: crate::storage::timeseries::ChunkId {
487 hypertable: hypertable.name.clone(),
488 start_ns: chunk.start_ns,
489 },
490 end_ns_exclusive: chunk.end_ns_exclusive,
491 row_count: chunk.row_count,
492 min_ts_ns: chunk.min_ts_ns,
493 max_ts_ns: chunk.max_ts_ns,
494 sealed: chunk.sealed,
495 ttl_override_ns: chunk.ttl_override_ns,
496 columnar_page: chunk.columnar_page,
497 };
498 let id = meta.id.clone();
499 let columnar_page = meta.columnar_page;
500 registry.restore_chunk(meta);
501 if let Some(loc) = columnar_page {
502 if let Ok(Some(bytes)) = self.read_column_block_page(loc) {
503 registry.restore_columnar_block(&id, bytes);
504 }
505 }
506 }
507 }
508 }
509
510 pub fn run_maintenance(&self) -> Result<(), Box<dyn std::error::Error>> {
511 self.store.run_maintenance()?;
512 self.persist_metadata()?;
513 Ok(())
514 }
515
516 pub fn metadata_path(&self) -> Option<PathBuf> {
518 self.path
519 .as_ref()
520 .map(|path| PhysicalMetadataFile::metadata_path_for(path))
521 }
522
523 pub fn physical_metadata(&self) -> Option<PhysicalMetadataFile> {
525 self.load_or_bootstrap_physical_metadata(!self.options.read_only)
526 .ok()
527 }
528
529 pub fn physical_indexes(&self) -> Vec<PhysicalIndexState> {
531 let indexes = self
532 .physical_metadata()
533 .map(|metadata| metadata.indexes)
534 .filter(|indexes| !indexes.is_empty())
535 .or_else(|| {
536 self.native_physical_state()
537 .map(|state| self.physical_index_state_from_native_state(&state, None))
538 })
539 .unwrap_or_else(|| self.physical_index_state());
540 self.reconcile_index_states_with_native_artifacts(indexes)
541 }
542
543 pub fn exports(&self) -> Vec<ExportDescriptor> {
545 self.physical_metadata()
546 .map(|metadata| metadata.exports)
547 .or_else(|| {
548 self.native_physical_state()
549 .map(|state| self.exports_from_native_state(&state))
550 })
551 .unwrap_or_default()
552 }
553
554 pub fn snapshots(&self) -> Vec<crate::physical::SnapshotDescriptor> {
556 if let Some(metadata) = self.physical_metadata() {
557 if !metadata.snapshots.is_empty() {
558 return metadata.snapshots;
559 }
560 }
561 self.native_physical_state()
562 .map(|state| self.snapshots_from_native_state(&state))
563 .unwrap_or_default()
564 }
565
566 pub fn graph_projections(&self) -> Vec<PhysicalGraphProjection> {
568 self.physical_metadata()
569 .map(|metadata| metadata.graph_projections)
570 .or_else(|| {
571 self.native_physical_state()
572 .map(|state| self.graph_projections_from_native_state(&state))
573 })
574 .unwrap_or_default()
575 }
576
577 pub fn declared_graph_projections(&self) -> Vec<PhysicalGraphProjection> {
579 self.catalog_model_snapshot().declared_graph_projections
580 }
581
582 pub fn operational_graph_projections(&self) -> Vec<PhysicalGraphProjection> {
584 self.graph_projections()
585 .into_iter()
586 .filter(|projection| {
587 projection.last_materialized_sequence.is_some()
588 || matches!(projection.state.as_str(), "materialized" | "stale")
589 })
590 .collect()
591 }
592
593 pub fn analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
595 self.physical_metadata()
596 .map(|metadata| metadata.analytics_jobs)
597 .or_else(|| {
598 self.native_physical_state()
599 .map(|state| self.analytics_jobs_from_native_state(&state))
600 })
601 .unwrap_or_default()
602 }
603
604 pub fn declared_analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
606 self.catalog_model_snapshot().declared_analytics_jobs
607 }
608
609 pub fn operational_analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
611 self.analytics_jobs()
612 .into_iter()
613 .filter(|job| {
614 job.last_run_sequence.is_some()
615 || matches!(
616 job.state.as_str(),
617 "running" | "completed" | "failed" | "queued" | "stale"
618 )
619 })
620 .collect()
621 }
622
623 pub fn declared_indexes(&self) -> Vec<PhysicalIndexState> {
625 self.catalog_model_snapshot().declared_indexes
626 }
627
628 pub fn operational_indexes(&self) -> Vec<PhysicalIndexState> {
630 self.catalog_model_snapshot().operational_indexes
631 }
632
633 pub fn index_statuses(&self) -> Vec<CatalogIndexStatus> {
635 self.catalog_model_snapshot().index_statuses
636 }
637
638 pub fn index_status(&self, name: &str) -> Option<CatalogIndexStatus> {
640 self.catalog_model_snapshot()
641 .index_statuses
642 .into_iter()
643 .find(|status| status.name == name)
644 }
645
646 pub fn save_graph_projection(
648 &self,
649 name: impl Into<String>,
650 node_labels: Vec<String>,
651 node_types: Vec<String>,
652 edge_labels: Vec<String>,
653 source: impl Into<String>,
654 ) -> Result<PhysicalGraphProjection, Box<dyn std::error::Error>> {
655 let name = name.into();
656 let source = source.into();
657 self.update_physical_metadata(|metadata| {
658 let now = SystemTime::now()
659 .duration_since(UNIX_EPOCH)
660 .unwrap_or_default()
661 .as_millis();
662 let projection = if let Some(existing) = metadata
663 .graph_projections
664 .iter_mut()
665 .find(|projection| projection.name == name)
666 {
667 existing.updated_at_unix_ms = now;
668 existing.state = "declared".to_string();
669 existing.source = source.clone();
670 existing.node_labels = node_labels.clone();
671 existing.node_types = node_types.clone();
672 existing.edge_labels = edge_labels.clone();
673 existing.last_materialized_sequence = None;
674 existing.clone()
675 } else {
676 let projection = PhysicalGraphProjection {
677 name: name.clone(),
678 created_at_unix_ms: now,
679 updated_at_unix_ms: now,
680 state: "declared".to_string(),
681 source: source.clone(),
682 node_labels: node_labels.clone(),
683 node_types: node_types.clone(),
684 edge_labels: edge_labels.clone(),
685 last_materialized_sequence: None,
686 };
687 metadata.graph_projections.push(projection.clone());
688 projection
689 };
690
691 Self::mark_projection_dependent_jobs_stale(metadata, &name, now);
692
693 metadata
694 .graph_projections
695 .sort_by(|left, right| left.name.cmp(&right.name));
696 projection
697 })
698 }
699
700 pub fn materialize_graph_projection(
702 &self,
703 name: &str,
704 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
705 self.update_physical_metadata(|metadata| {
706 let now = SystemTime::now()
707 .duration_since(UNIX_EPOCH)
708 .unwrap_or_default()
709 .as_millis();
710 let idx = metadata
711 .graph_projections
712 .iter()
713 .position(|projection| projection.name == name);
714 if let Some(idx) = idx {
715 metadata.graph_projections[idx].updated_at_unix_ms = now;
716 metadata.graph_projections[idx].state = "materialized".to_string();
717 metadata.graph_projections[idx].last_materialized_sequence =
718 Some(metadata.superblock.sequence);
719 let result = metadata.graph_projections[idx].clone();
720 Self::rearm_projection_dependent_jobs_declared(metadata, name, now);
721 return Some(result);
722 }
723 None
724 })
725 }
726
727 pub fn mark_graph_projection_materializing(
729 &self,
730 name: &str,
731 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
732 self.update_physical_metadata(|metadata| {
733 let now = SystemTime::now()
734 .duration_since(UNIX_EPOCH)
735 .unwrap_or_default()
736 .as_millis();
737 let idx = metadata
738 .graph_projections
739 .iter()
740 .position(|projection| projection.name == name);
741 if let Some(idx) = idx {
742 metadata.graph_projections[idx].updated_at_unix_ms = now;
743 metadata.graph_projections[idx].state = "materializing".to_string();
744 metadata.graph_projections[idx].last_materialized_sequence = None;
745 let result = metadata.graph_projections[idx].clone();
746 Self::mark_projection_dependent_jobs_stale(metadata, name, now);
747 return Some(result);
748 }
749 None
750 })
751 }
752
753 pub fn fail_graph_projection(
755 &self,
756 name: &str,
757 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
758 self.update_physical_metadata(|metadata| {
759 let now = SystemTime::now()
760 .duration_since(UNIX_EPOCH)
761 .unwrap_or_default()
762 .as_millis();
763 let idx = metadata
764 .graph_projections
765 .iter()
766 .position(|projection| projection.name == name);
767 if let Some(idx) = idx {
768 metadata.graph_projections[idx].updated_at_unix_ms = now;
769 metadata.graph_projections[idx].state = "failed".to_string();
770 metadata.graph_projections[idx].last_materialized_sequence = None;
771 let result = metadata.graph_projections[idx].clone();
772 Self::mark_projection_dependent_jobs_stale(metadata, name, now);
773 return Some(result);
774 }
775 None
776 })
777 }
778
779 pub fn mark_graph_projection_stale(
781 &self,
782 name: &str,
783 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
784 self.update_physical_metadata(|metadata| {
785 let now = SystemTime::now()
786 .duration_since(UNIX_EPOCH)
787 .unwrap_or_default()
788 .as_millis();
789 let idx = metadata
790 .graph_projections
791 .iter()
792 .position(|projection| projection.name == name);
793 if let Some(idx) = idx {
794 metadata.graph_projections[idx].updated_at_unix_ms = now;
795 metadata.graph_projections[idx].state = "stale".to_string();
796 let result = metadata.graph_projections[idx].clone();
797 Self::mark_projection_dependent_jobs_stale(metadata, name, now);
798 return Some(result);
799 }
800 None
801 })
802 }
803
804 fn mark_projection_dependent_jobs_stale(
805 metadata: &mut PhysicalMetadataFile,
806 projection_name: &str,
807 now: u128,
808 ) {
809 for job in metadata.analytics_jobs.iter_mut() {
810 if job.projection.as_deref() == Some(projection_name) && job.state != "declared" {
811 job.state = "stale".to_string();
812 job.updated_at_unix_ms = now;
813 }
814 }
815 }
816
817 fn rearm_projection_dependent_jobs_declared(
818 metadata: &mut PhysicalMetadataFile,
819 projection_name: &str,
820 now: u128,
821 ) {
822 for job in metadata.analytics_jobs.iter_mut() {
823 if job.projection.as_deref() == Some(projection_name) && job.state == "stale" {
824 job.state = "declared".to_string();
825 job.last_run_sequence = None;
826 job.updated_at_unix_ms = now;
827 }
828 }
829 }
830
831 pub fn save_analytics_job(
833 &self,
834 kind: impl Into<String>,
835 projection: Option<String>,
836 metadata_entries: BTreeMap<String, String>,
837 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
838 let kind = kind.into();
839 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
840 if let Some(projection_name) = projection.as_deref() {
841 if !self.graph_projection_is_declared(projection_name) {
842 return Err(format!(
843 "graph projection '{projection_name}' is not declared in physical metadata"
844 )
845 .into());
846 }
847 }
848
849 self.update_physical_metadata(|metadata| {
850 let now = SystemTime::now()
851 .duration_since(UNIX_EPOCH)
852 .unwrap_or_default()
853 .as_millis();
854
855 let job = if let Some(existing) = metadata
856 .analytics_jobs
857 .iter_mut()
858 .find(|job| job.id == job_id)
859 {
860 existing.kind = kind.clone();
861 existing.projection = projection.clone();
862 existing.updated_at_unix_ms = now;
863 existing.metadata = metadata_entries.clone();
864 if existing.last_run_sequence.is_none() {
865 existing.state = "declared".to_string();
866 }
867 existing.clone()
868 } else {
869 let job = PhysicalAnalyticsJob {
870 id: job_id.clone(),
871 kind: kind.clone(),
872 state: "declared".to_string(),
873 projection: projection.clone(),
874 created_at_unix_ms: now,
875 updated_at_unix_ms: now,
876 last_run_sequence: None,
877 metadata: metadata_entries.clone(),
878 };
879 metadata.analytics_jobs.push(job.clone());
880 job
881 };
882
883 metadata
884 .analytics_jobs
885 .sort_by(|left, right| left.id.cmp(&right.id));
886 job
887 })
888 }
889
890 pub fn record_analytics_job(
892 &self,
893 kind: impl Into<String>,
894 projection: Option<String>,
895 metadata_entries: BTreeMap<String, String>,
896 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
897 let kind = kind.into();
898 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
899 if let Some(projection_name) = projection.as_deref() {
900 if !self.graph_projection_is_declared(projection_name) {
901 return Err(format!(
902 "graph projection '{projection_name}' is not declared in physical metadata"
903 )
904 .into());
905 }
906 if !self.graph_projection_is_operational(projection_name) {
907 return Err(format!(
908 "graph projection '{projection_name}' is declared but not operationally materialized"
909 )
910 .into());
911 }
912 }
913
914 self.update_physical_metadata(|metadata| {
915 let now = SystemTime::now()
916 .duration_since(UNIX_EPOCH)
917 .unwrap_or_default()
918 .as_millis();
919
920 let existing = metadata
921 .analytics_jobs
922 .iter_mut()
923 .find(|job| job.id == job_id)?;
924 existing.state = "completed".to_string();
925 existing.kind = kind.clone();
926 existing.projection = projection.clone();
927 existing.updated_at_unix_ms = now;
928 existing.last_run_sequence = Some(metadata.superblock.sequence);
929 existing.metadata = metadata_entries.clone();
930 let job = existing.clone();
931
932 metadata
933 .analytics_jobs
934 .sort_by(|left, right| left.id.cmp(&right.id));
935 Some(job)
936 })
937 .and_then(|job| {
938 job.ok_or_else(|| {
939 format!("analytics job '{job_id}' is not declared in physical metadata").into()
940 })
941 })
942 }
943
944 pub fn queue_analytics_job(
946 &self,
947 kind: impl Into<String>,
948 projection: Option<String>,
949 metadata_entries: BTreeMap<String, String>,
950 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
951 let kind = kind.into();
952 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
953 if let Some(projection_name) = projection.as_deref() {
954 if !self.graph_projection_is_declared(projection_name) {
955 return Err(format!(
956 "graph projection '{projection_name}' is not declared in physical metadata"
957 )
958 .into());
959 }
960 if !self.graph_projection_is_operational(projection_name) {
961 return Err(format!(
962 "graph projection '{projection_name}' is declared but not operationally materialized"
963 )
964 .into());
965 }
966 }
967
968 self.update_physical_metadata(|metadata| {
969 let now = SystemTime::now()
970 .duration_since(UNIX_EPOCH)
971 .unwrap_or_default()
972 .as_millis();
973
974 let existing = metadata
975 .analytics_jobs
976 .iter_mut()
977 .find(|job| job.id == job_id)?;
978 existing.state = "queued".to_string();
979 existing.kind = kind.clone();
980 existing.projection = projection.clone();
981 existing.updated_at_unix_ms = now;
982 existing.metadata = metadata_entries.clone();
983 let job = existing.clone();
984
985 metadata
986 .analytics_jobs
987 .sort_by(|left, right| left.id.cmp(&right.id));
988 Some(job)
989 })
990 .and_then(|job| {
991 job.ok_or_else(|| {
992 format!("analytics job '{job_id}' is not declared in physical metadata").into()
993 })
994 })
995 }
996
997 pub fn start_analytics_job(
999 &self,
1000 kind: impl Into<String>,
1001 projection: Option<String>,
1002 metadata_entries: BTreeMap<String, String>,
1003 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1004 let kind = kind.into();
1005 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1006 if let Some(projection_name) = projection.as_deref() {
1007 if !self.graph_projection_is_declared(projection_name) {
1008 return Err(format!(
1009 "graph projection '{projection_name}' is not declared in physical metadata"
1010 )
1011 .into());
1012 }
1013 if !self.graph_projection_is_operational(projection_name) {
1014 return Err(format!(
1015 "graph projection '{projection_name}' is declared but not operationally materialized"
1016 )
1017 .into());
1018 }
1019 }
1020
1021 self.update_physical_metadata(|metadata| {
1022 let now = SystemTime::now()
1023 .duration_since(UNIX_EPOCH)
1024 .unwrap_or_default()
1025 .as_millis();
1026
1027 let existing = metadata
1028 .analytics_jobs
1029 .iter_mut()
1030 .find(|job| job.id == job_id)?;
1031 existing.state = "running".to_string();
1032 existing.kind = kind.clone();
1033 existing.projection = projection.clone();
1034 existing.updated_at_unix_ms = now;
1035 existing.metadata = metadata_entries.clone();
1036 let job = existing.clone();
1037
1038 metadata
1039 .analytics_jobs
1040 .sort_by(|left, right| left.id.cmp(&right.id));
1041 Some(job)
1042 })
1043 .and_then(|job| {
1044 job.ok_or_else(|| {
1045 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1046 })
1047 })
1048 }
1049
1050 pub fn fail_analytics_job(
1052 &self,
1053 kind: impl Into<String>,
1054 projection: Option<String>,
1055 metadata_entries: BTreeMap<String, String>,
1056 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1057 let kind = kind.into();
1058 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1059
1060 self.update_physical_metadata(|metadata| {
1061 let now = SystemTime::now()
1062 .duration_since(UNIX_EPOCH)
1063 .unwrap_or_default()
1064 .as_millis();
1065
1066 let existing = metadata
1067 .analytics_jobs
1068 .iter_mut()
1069 .find(|job| job.id == job_id)?;
1070 existing.state = "failed".to_string();
1071 existing.kind = kind.clone();
1072 existing.projection = projection.clone();
1073 existing.updated_at_unix_ms = now;
1074 existing.metadata = metadata_entries.clone();
1075 let job = existing.clone();
1076
1077 metadata
1078 .analytics_jobs
1079 .sort_by(|left, right| left.id.cmp(&right.id));
1080 Some(job)
1081 })
1082 .and_then(|job| {
1083 job.ok_or_else(|| {
1084 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1085 })
1086 })
1087 }
1088
1089 pub fn mark_analytics_job_stale(
1091 &self,
1092 kind: impl Into<String>,
1093 projection: Option<String>,
1094 metadata_entries: BTreeMap<String, String>,
1095 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1096 let kind = kind.into();
1097 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1098
1099 self.update_physical_metadata(|metadata| {
1100 let now = SystemTime::now()
1101 .duration_since(UNIX_EPOCH)
1102 .unwrap_or_default()
1103 .as_millis();
1104
1105 let existing = metadata
1106 .analytics_jobs
1107 .iter_mut()
1108 .find(|job| job.id == job_id)?;
1109 existing.state = "stale".to_string();
1110 existing.kind = kind.clone();
1111 existing.projection = projection.clone();
1112 existing.updated_at_unix_ms = now;
1113 existing.metadata = metadata_entries.clone();
1114 let job = existing.clone();
1115
1116 metadata
1117 .analytics_jobs
1118 .sort_by(|left, right| left.id.cmp(&right.id));
1119 Some(job)
1120 })
1121 .and_then(|job| {
1122 job.ok_or_else(|| {
1123 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1124 })
1125 })
1126 }
1127
1128 pub fn create_named_export(
1130 &self,
1131 name: impl Into<String>,
1132 ) -> Result<ExportDescriptor, Box<dyn std::error::Error>> {
1133 let name = name.into();
1134 if self.options.mode != StorageMode::Persistent {
1135 return Err("exports require persistent mode".into());
1136 }
1137 let Some(path) = self.path() else {
1138 return Err("database path is not available".into());
1139 };
1140
1141 self.flush()?;
1142
1143 let _publication = self.store.vault_publication_lock.lock();
1144 let mut metadata = match self.load_or_bootstrap_physical_metadata(true) {
1145 Ok(metadata) => metadata,
1146 Err(_)
1147 if self.options.storage_profile.deploy_profile
1148 == crate::storage::DeployProfile::Embedded
1149 && self.options.storage_profile.packaging
1150 == crate::storage::StoragePackaging::SingleFile =>
1151 {
1152 crate::physical::PhysicalMetadataFile::from_state(
1153 self.options.clone(),
1154 self.catalog_snapshot(),
1155 self.physical_collection_roots(),
1156 self.physical_index_state(),
1157 None,
1158 )
1159 }
1160 Err(err) => return Err(err),
1161 };
1162 let export_data_path = reddb_file::copy_physical_export_data_file(path, &name)?;
1163 let export_metadata_path = PhysicalMetadataFile::metadata_path_for(&export_data_path);
1164 let export_metadata_binary_path =
1165 PhysicalMetadataFile::metadata_binary_path_for(&export_data_path);
1166
1167 let descriptor = ExportDescriptor {
1168 name: name.clone(),
1169 created_at_unix_ms: SystemTime::now()
1170 .duration_since(UNIX_EPOCH)
1171 .unwrap_or_default()
1172 .as_millis(),
1173 snapshot_id: metadata
1174 .snapshots
1175 .last()
1176 .map(|snapshot| snapshot.snapshot_id),
1177 superblock_sequence: metadata.superblock.sequence,
1178 data_path: export_data_path.display().to_string(),
1179 metadata_path: export_metadata_path.display().to_string(),
1180 collection_count: metadata.catalog.total_collections,
1181 total_entities: metadata.catalog.total_entities,
1182 };
1183
1184 metadata
1185 .exports
1186 .retain(|export| export.name != descriptor.name);
1187 metadata.exports.push(descriptor.clone());
1188 self.prune_export_registry(&mut metadata.exports);
1189 metadata.save_for_data_path(path)?;
1190 metadata.save_to_binary_path(&export_metadata_binary_path)?;
1191 metadata.save_to_path(&export_metadata_path)?;
1192
1193 Ok(descriptor)
1194 }
1195
1196 pub fn set_index_enabled(
1198 &self,
1199 name: &str,
1200 enabled: bool,
1201 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1202 let Some(status) = self.index_status(name) else {
1203 return Err(format!("index '{name}' is not present in catalog status").into());
1204 };
1205 if !status.declared {
1206 return Err(format!("index '{name}' is not declared in physical metadata").into());
1207 }
1208 self.update_physical_metadata(|metadata| {
1209 let now = SystemTime::now()
1210 .duration_since(UNIX_EPOCH)
1211 .unwrap_or_default()
1212 .as_millis();
1213 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1214 index.enabled = enabled;
1215 if !enabled {
1216 index.build_state = "disabled".to_string();
1217 } else if index.build_state == "disabled" {
1218 index.build_state = if index.artifact_root_page.is_some() {
1219 "ready".to_string()
1220 } else {
1221 "declared-unbuilt".to_string()
1222 };
1223 }
1224 index.last_refresh_ms = Some(now);
1225 return Some(index.clone());
1226 }
1227 None
1228 })
1229 }
1230
1231 pub fn mark_index_building(
1233 &self,
1234 name: &str,
1235 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1236 let Some(status) = self.index_status(name) else {
1237 return Err(format!("index '{name}' is not present in catalog status").into());
1238 };
1239 if !status.declared {
1240 return Err(format!("index '{name}' is not declared in physical metadata").into());
1241 }
1242 if status.lifecycle_state == "disabled" {
1243 return Err(format!("index '{name}' is disabled").into());
1244 }
1245 self.update_physical_metadata(|metadata| {
1246 let now = SystemTime::now()
1247 .duration_since(UNIX_EPOCH)
1248 .unwrap_or_default()
1249 .as_millis();
1250 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1251 index.build_state = "building".to_string();
1252 index.last_refresh_ms = Some(now);
1253 return Some(index.clone());
1254 }
1255 None
1256 })
1257 }
1258
1259 pub fn fail_index(
1261 &self,
1262 name: &str,
1263 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1264 let Some(status) = self.index_status(name) else {
1265 return Err(format!("index '{name}' is not present in catalog status").into());
1266 };
1267 if !status.declared {
1268 return Err(format!("index '{name}' is not declared in physical metadata").into());
1269 }
1270 self.update_physical_metadata(|metadata| {
1271 let now = SystemTime::now()
1272 .duration_since(UNIX_EPOCH)
1273 .unwrap_or_default()
1274 .as_millis();
1275 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1276 index.build_state = "failed".to_string();
1277 index.last_refresh_ms = Some(now);
1278 return Some(index.clone());
1279 }
1280 None
1281 })
1282 }
1283
1284 pub fn mark_index_stale(
1286 &self,
1287 name: &str,
1288 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1289 let Some(status) = self.index_status(name) else {
1290 return Err(format!("index '{name}' is not present in catalog status").into());
1291 };
1292 if !status.declared {
1293 return Err(format!("index '{name}' is not declared in physical metadata").into());
1294 }
1295 self.update_physical_metadata(|metadata| {
1296 let now = SystemTime::now()
1297 .duration_since(UNIX_EPOCH)
1298 .unwrap_or_default()
1299 .as_millis();
1300 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1301 index.build_state = if index.enabled {
1302 "stale".to_string()
1303 } else {
1304 "disabled".to_string()
1305 };
1306 index.last_refresh_ms = Some(now);
1307 return Some(index.clone());
1308 }
1309 None
1310 })
1311 }
1312
1313 pub fn mark_index_ready(
1315 &self,
1316 name: &str,
1317 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1318 self.warmup_index(name)
1319 }
1320
1321 pub fn warmup_index(
1323 &self,
1324 name: &str,
1325 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1326 let Some(status) = self.index_status(name) else {
1327 return Err(format!("index '{name}' is not present in catalog status").into());
1328 };
1329 if !status.declared {
1330 return Err(format!("index '{name}' is not declared in physical metadata").into());
1331 }
1332 if status.lifecycle_state == "disabled" {
1333 return Err(format!("index '{name}' is disabled").into());
1334 }
1335 if !status.operational {
1336 return Err(
1337 format!("index '{name}' is declared but not operationally materialized").into(),
1338 );
1339 }
1340 let warmed_artifact = self
1341 .physical_indexes()
1342 .into_iter()
1343 .find(|index| index.name == name)
1344 .map(|mut index| {
1345 self.warmup_native_vector_artifact_for_index(&index)?;
1346 self.apply_runtime_native_artifact_to_index_state(&mut index)?;
1347 Ok::<_, String>(index)
1348 })
1349 .transpose()
1350 .map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
1351
1352 self.update_physical_metadata(|metadata| {
1353 let now = SystemTime::now()
1354 .duration_since(UNIX_EPOCH)
1355 .unwrap_or_default()
1356 .as_millis();
1357 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1358 if let Some(warmed) = warmed_artifact.as_ref() {
1359 index.entries = warmed.entries;
1360 index.estimated_memory_bytes = warmed.estimated_memory_bytes;
1361 index.backend = warmed.backend.clone();
1362 index.build_state = "ready".to_string();
1363 }
1364 index.last_refresh_ms = Some(now);
1365 return Some(index.clone());
1366 }
1367 None
1368 })
1369 }
1370
1371 pub fn rebuild_index_registry(
1373 &self,
1374 collection: Option<&str>,
1375 ) -> Result<Vec<PhysicalIndexState>, Box<dyn std::error::Error>> {
1376 let fresh = self.reconcile_index_states_with_native_artifacts(self.physical_index_state());
1377 self.update_physical_metadata(|metadata| {
1378 let now = SystemTime::now()
1379 .duration_since(UNIX_EPOCH)
1380 .unwrap_or_default()
1381 .as_millis();
1382
1383 let mut affected = Vec::new();
1384 let declared = metadata.indexes.clone();
1385 for declared_index in declared {
1386 let matches_collection = collection.is_none_or(|collection_name| {
1387 declared_index.collection.as_deref() == Some(collection_name)
1388 });
1389 if !matches_collection {
1390 continue;
1391 }
1392
1393 let mut rebuilt = fresh
1394 .iter()
1395 .find(|index| index.name == declared_index.name)
1396 .cloned()
1397 .unwrap_or_else(|| {
1398 let mut index = declared_index.clone();
1399 index.build_state = "declared-unbuilt".to_string();
1400 index
1401 });
1402 rebuilt.enabled = declared_index.enabled;
1403 rebuilt.artifact_kind = rebuilt
1404 .artifact_kind
1405 .or_else(|| declared_index.artifact_kind.clone());
1406 rebuilt.artifact_root_page = rebuilt
1407 .artifact_root_page
1408 .or(declared_index.artifact_root_page);
1409 rebuilt.artifact_checksum = rebuilt
1410 .artifact_checksum
1411 .or(declared_index.artifact_checksum);
1412 rebuilt.build_state =
1413 Self::finalize_rebuilt_index_build_state(&declared_index, &rebuilt);
1414 rebuilt.last_refresh_ms = Some(now);
1415
1416 if let Some(existing) = metadata
1417 .indexes
1418 .iter_mut()
1419 .find(|index| index.name == rebuilt.name)
1420 {
1421 *existing = rebuilt.clone();
1422 } else {
1423 metadata.indexes.push(rebuilt.clone());
1424 }
1425
1426 affected.push(rebuilt);
1427 }
1428
1429 affected
1430 })
1431 }
1432
1433 fn finalize_rebuilt_index_build_state(
1434 declared: &PhysicalIndexState,
1435 rebuilt: &PhysicalIndexState,
1436 ) -> String {
1437 if !rebuilt.enabled {
1438 return "disabled".to_string();
1439 }
1440
1441 if declared.build_state == "failed" || rebuilt.build_state == "failed" {
1442 return "failed".to_string();
1443 }
1444
1445 let native_artifact_family = Self::native_artifact_kind_for_index(rebuilt.kind).is_some();
1446 if native_artifact_family {
1447 if rebuilt.artifact_root_page.is_some() && rebuilt.artifact_checksum.is_some() {
1448 return "ready".to_string();
1449 }
1450 if declared.artifact_root_page.is_some()
1451 || declared.artifact_checksum.is_some()
1452 || declared.artifact_kind.is_some()
1453 {
1454 return "stale".to_string();
1455 }
1456 return "declared-unbuilt".to_string();
1457 }
1458
1459 if rebuilt.entries > 0 {
1460 return "ready".to_string();
1461 }
1462
1463 if matches!(
1464 declared.build_state.as_str(),
1465 "stale" | "artifact-published" | "registry-loaded"
1466 ) {
1467 return "stale".to_string();
1468 }
1469
1470 "declared-unbuilt".to_string()
1471 }
1472}