1use super::*;
2
3impl RedDB {
4 pub fn quorum_coordinator(
11 &self,
12 ) -> Option<&Arc<crate::replication::quorum::QuorumCoordinator>> {
13 self.quorum.as_ref()
14 }
15
16 pub fn wait_for_replication_quorum(
27 &self,
28 target_lsn: u64,
29 ) -> Result<(), crate::replication::QuorumError> {
30 match &self.quorum {
31 Some(q) => q.wait_for_quorum(target_lsn),
32 None => Ok(()),
33 }
34 }
35}
36
37impl RedDB {
38 pub(crate) fn native_registry_summary_from_metadata(
39 &self,
40 metadata: &PhysicalMetadataFile,
41 ) -> NativeRegistrySummary {
42 const SAMPLE_LIMIT: usize = 32;
43
44 let collection_names: Vec<_> = metadata
45 .catalog
46 .stats_by_collection
47 .keys()
48 .take(SAMPLE_LIMIT)
49 .cloned()
50 .collect();
51 let indexes: Vec<_> = metadata
52 .indexes
53 .iter()
54 .take(SAMPLE_LIMIT)
55 .map(|index| NativeRegistryIndexSummary {
56 name: index.name.clone(),
57 kind: index.kind.as_str().to_string(),
58 collection: index.collection.clone(),
59 enabled: index.enabled,
60 entries: index.entries as u64,
61 estimated_memory_bytes: index.estimated_memory_bytes,
62 last_refresh_ms: index.last_refresh_ms,
63 backend: index.backend.clone(),
64 })
65 .collect();
66 let graph_projections: Vec<_> = metadata
67 .graph_projections
68 .iter()
69 .take(SAMPLE_LIMIT)
70 .map(|projection| NativeRegistryProjectionSummary {
71 name: projection.name.clone(),
72 source: projection.source.clone(),
73 created_at_unix_ms: projection.created_at_unix_ms,
74 updated_at_unix_ms: projection.updated_at_unix_ms,
75 node_labels: projection.node_labels.clone(),
76 node_types: projection.node_types.clone(),
77 edge_labels: projection.edge_labels.clone(),
78 last_materialized_sequence: projection.last_materialized_sequence,
79 })
80 .collect();
81 let analytics_jobs = metadata
82 .analytics_jobs
83 .iter()
84 .take(SAMPLE_LIMIT)
85 .map(|job| NativeRegistryJobSummary {
86 id: job.id.clone(),
87 kind: job.kind.clone(),
88 projection: job.projection.clone(),
89 state: job.state.clone(),
90 created_at_unix_ms: job.created_at_unix_ms,
91 updated_at_unix_ms: job.updated_at_unix_ms,
92 last_run_sequence: job.last_run_sequence,
93 metadata: job.metadata.clone(),
94 })
95 .collect::<Vec<_>>();
96 let vector_artifacts = self
97 .native_vector_artifact_records()
98 .into_iter()
99 .map(|(summary, _)| summary)
100 .take(SAMPLE_LIMIT)
101 .collect::<Vec<_>>();
102 let vector_artifact_count = self.native_vector_artifact_collection_count() as u32;
103
104 NativeRegistrySummary {
105 collection_count: metadata.catalog.total_collections as u32,
106 index_count: metadata.indexes.len() as u32,
107 graph_projection_count: metadata.graph_projections.len() as u32,
108 analytics_job_count: metadata.analytics_jobs.len() as u32,
109 vector_artifact_count,
110 collections_complete: metadata.catalog.stats_by_collection.len() <= SAMPLE_LIMIT,
111 indexes_complete: metadata.indexes.len() <= SAMPLE_LIMIT,
112 graph_projections_complete: metadata.graph_projections.len() <= SAMPLE_LIMIT,
113 analytics_jobs_complete: metadata.analytics_jobs.len() <= SAMPLE_LIMIT,
114 vector_artifacts_complete: vector_artifact_count as usize <= SAMPLE_LIMIT,
115 omitted_collection_count: metadata
116 .catalog
117 .stats_by_collection
118 .len()
119 .saturating_sub(collection_names.len())
120 as u32,
121 omitted_index_count: metadata.indexes.len().saturating_sub(indexes.len()) as u32,
122 omitted_graph_projection_count: metadata
123 .graph_projections
124 .len()
125 .saturating_sub(graph_projections.len())
126 as u32,
127 omitted_analytics_job_count: metadata
128 .analytics_jobs
129 .len()
130 .saturating_sub(analytics_jobs.len())
131 as u32,
132 omitted_vector_artifact_count: vector_artifact_count
133 .saturating_sub(vector_artifacts.len() as u32),
134 collection_names,
135 indexes,
136 graph_projections,
137 analytics_jobs,
138 vector_artifacts,
139 }
140 }
141
142 fn native_vector_artifact_collection_count(&self) -> usize {
143 self.native_vector_artifact_records().len()
144 }
145
146 pub(crate) fn native_vector_artifact_records(
147 &self,
148 ) -> Vec<(NativeVectorArtifactSummary, Vec<u8>)> {
149 let mut artifacts = Vec::new();
150 for collection in self.store.list_collections() {
151 let Some(manager) = self.store.get_collection(&collection) else {
152 continue;
153 };
154 let entities = manager.query_all(|_| true);
155 let mut vectors = Vec::new();
156 let mut graph_edges = Vec::new();
157 let mut fulltext_documents = Vec::new();
158 let mut document_records = Vec::new();
159 for entity in entities {
160 match entity.data {
161 EntityData::Vector(vector) => {
162 if !vector.dense.is_empty() {
163 vectors.push((entity.id, vector.dense));
164 }
165 }
166 EntityData::Edge(edge) => {
167 if let EntityKind::GraphEdge(edge_kind) = entity.kind {
168 graph_edges.push((
169 entity.id,
170 edge_kind.from_node,
171 edge_kind.to_node,
172 edge_kind.label,
173 edge.weight,
174 ));
175 }
176 }
177 data => {
178 let text = Self::native_fulltext_text_for_entity(&data);
179 if !text.trim().is_empty() {
180 fulltext_documents.push((entity.id, text));
181 }
182 if let Some(document) =
183 Self::native_document_pathvalue_for_entity(entity.id, &data)
184 {
185 document_records.push(document);
186 }
187 }
188 }
189 }
190 if !vectors.is_empty() {
191 let dimension = vectors[0].1.len();
192 let mut hnsw = HnswIndex::with_dimension(dimension);
193 for (id, vector) in vectors
194 .into_iter()
195 .filter(|(_, vector)| vector.len() == dimension)
196 {
197 hnsw.insert_with_id(id.raw(), vector);
198 }
199 let stats = hnsw.stats();
200 let bytes = hnsw.to_bytes();
201 let summary = NativeVectorArtifactSummary {
202 collection: collection.clone(),
203 artifact_kind: "hnsw".to_string(),
204 vector_count: stats.node_count as u64,
205 dimension: stats.dimension as u32,
206 max_layer: stats.max_layer as u32,
207 serialized_bytes: bytes.len() as u64,
208 checksum: crate::storage::engine::crc32(&bytes) as u64,
209 };
210 artifacts.push((summary, bytes));
211
212 let n_lists = ((stats.node_count as f64).sqrt().ceil() as usize).max(1);
213 let mut ivf = IvfIndex::new(IvfConfig::new(dimension, n_lists));
214 let training = manager
215 .query_all(|_| true)
216 .into_iter()
217 .filter_map(|entity| match entity.data {
218 EntityData::Vector(vector) if vector.dense.len() == dimension => {
219 Some(vector.dense)
220 }
221 _ => None,
222 })
223 .collect::<Vec<_>>();
224 ivf.train(&training);
225 let items = manager
226 .query_all(|_| true)
227 .into_iter()
228 .filter_map(|entity| match entity.data {
229 EntityData::Vector(vector) if vector.dense.len() == dimension => {
230 Some((entity.id.raw(), vector.dense))
231 }
232 _ => None,
233 })
234 .collect::<Vec<_>>();
235 ivf.add_batch_with_ids(items);
236 let ivf_stats = ivf.stats();
237 let ivf_bytes = ivf.to_bytes();
238 let ivf_summary = NativeVectorArtifactSummary {
239 collection: collection.clone(),
240 artifact_kind: "ivf".to_string(),
241 vector_count: ivf_stats.total_vectors as u64,
242 dimension: ivf_stats.dimension as u32,
243 max_layer: ivf_stats.n_lists as u32,
244 serialized_bytes: ivf_bytes.len() as u64,
245 checksum: crate::storage::engine::crc32(&ivf_bytes) as u64,
246 };
247 artifacts.push((ivf_summary, ivf_bytes));
248 }
249
250 if !graph_edges.is_empty() {
251 let bytes = Self::serialize_native_graph_adjacency_artifact(&graph_edges);
252 let (edge_count, node_count, label_count) =
253 Self::inspect_native_graph_adjacency_artifact(&bytes).unwrap_or((0, 0, 0));
254 let summary = NativeVectorArtifactSummary {
255 collection: collection.clone(),
256 artifact_kind: "graph.adjacency".to_string(),
257 vector_count: edge_count,
258 dimension: node_count as u32,
259 max_layer: label_count,
260 serialized_bytes: bytes.len() as u64,
261 checksum: crate::storage::engine::crc32(&bytes) as u64,
262 };
263 artifacts.push((summary, bytes));
264 }
265
266 if !fulltext_documents.is_empty() {
267 let bytes =
268 Self::serialize_native_fulltext_artifact(&collection, &fulltext_documents);
269 let (doc_count, term_count, posting_count) =
270 Self::inspect_native_fulltext_artifact(&bytes).unwrap_or((0, 0, 0));
271 let summary = NativeVectorArtifactSummary {
272 collection: collection.clone(),
273 artifact_kind: "text.fulltext".to_string(),
274 vector_count: posting_count,
275 dimension: doc_count as u32,
276 max_layer: term_count as u32,
277 serialized_bytes: bytes.len() as u64,
278 checksum: crate::storage::engine::crc32(&bytes) as u64,
279 };
280 artifacts.push((summary, bytes));
281 }
282
283 if !document_records.is_empty() {
284 let bytes = Self::serialize_native_document_pathvalue_artifact(
285 &collection,
286 &document_records,
287 );
288 let (doc_count, path_count, value_count, unique_value_count) =
289 Self::inspect_native_document_pathvalue_artifact(&bytes)
290 .unwrap_or((0, 0, 0, 0));
291 let _ = unique_value_count;
292 let summary = NativeVectorArtifactSummary {
293 collection: collection.clone(),
294 artifact_kind: "document.pathvalue".to_string(),
295 vector_count: value_count,
296 dimension: doc_count as u32,
297 max_layer: path_count as u32,
298 serialized_bytes: bytes.len() as u64,
299 checksum: crate::storage::engine::crc32(&bytes) as u64,
300 };
301 artifacts.push((summary, bytes));
302 }
303 }
304 artifacts
305 }
306
307 fn serialize_native_graph_adjacency_artifact(
308 edges: &[(EntityId, String, String, String, f32)],
309 ) -> Vec<u8> {
310 let edges: Vec<reddb_file::NativeGraphEdge> = edges
311 .iter()
312 .map(
313 |(edge_id, from_node, to_node, label, weight)| reddb_file::NativeGraphEdge {
314 edge_id: edge_id.raw(),
315 from_node: from_node.clone(),
316 to_node: to_node.clone(),
317 label: label.clone(),
318 weight: *weight,
319 },
320 )
321 .collect();
322 reddb_file::encode_native_graph_adjacency_frame(&reddb_file::NativeGraphAdjacencyFrame {
323 edges,
324 })
325 }
326
327 pub(crate) fn inspect_native_graph_adjacency_artifact(
328 bytes: &[u8],
329 ) -> Result<(u64, u64, u32), String> {
330 let frame =
331 reddb_file::decode_native_graph_adjacency_frame(bytes).map_err(|e| e.to_string())?;
332 let edge_count = frame.edges.len() as u64;
333 let mut nodes = BTreeSet::new();
334 let mut labels = BTreeSet::new();
335 for edge in frame.edges {
336 nodes.insert(edge.from_node);
337 nodes.insert(edge.to_node);
338 labels.insert(edge.label);
339 }
340 Ok((edge_count, nodes.len() as u64, labels.len() as u32))
341 }
342
343 fn serialize_native_fulltext_artifact(
344 collection: &str,
345 documents: &[(EntityId, String)],
346 ) -> Vec<u8> {
347 let mut postings: BTreeMap<String, Vec<(u64, u32)>> = BTreeMap::new();
348 for (entity_id, text) in documents {
349 let mut frequencies: BTreeMap<String, u32> = BTreeMap::new();
350 for token in Self::native_fulltext_tokenize(text) {
351 *frequencies.entry(token).or_insert(0) += 1;
352 }
353 for (token, count) in frequencies {
354 postings
355 .entry(token)
356 .or_default()
357 .push((entity_id.raw(), count));
358 }
359 }
360
361 let terms = postings
362 .into_iter()
363 .map(|(term, postings)| reddb_file::NativeFulltextTerm {
364 term,
365 postings: postings
366 .into_iter()
367 .map(
368 |(entity_id, term_count)| reddb_file::NativeFulltextPosting {
369 entity_id,
370 term_count,
371 },
372 )
373 .collect(),
374 })
375 .collect();
376 reddb_file::encode_native_fulltext_frame(&reddb_file::NativeFulltextFrame {
377 collection: collection.to_string(),
378 doc_count: documents.len() as u32,
379 terms,
380 })
381 }
382
383 pub(crate) fn inspect_native_fulltext_artifact(
384 bytes: &[u8],
385 ) -> Result<(u64, u64, u64), String> {
386 let frame = reddb_file::decode_native_fulltext_frame(bytes).map_err(|e| e.to_string())?;
387 let posting_count: u64 = frame
388 .terms
389 .iter()
390 .map(|term| term.postings.len() as u64)
391 .sum();
392 Ok((
393 frame.doc_count as u64,
394 frame.terms.len() as u64,
395 posting_count,
396 ))
397 }
398
399 fn serialize_native_document_pathvalue_artifact(
400 collection: &str,
401 documents: &[(EntityId, Vec<(String, String)>)],
402 ) -> Vec<u8> {
403 let docs: Vec<reddb_file::NativeDocPathValue> = documents
404 .iter()
405 .map(|(entity_id, entries)| reddb_file::NativeDocPathValue {
406 entity_id: entity_id.raw(),
407 entries: entries
408 .iter()
409 .map(|(path, value)| reddb_file::NativeDocPathValueEntry {
410 path: path.clone(),
411 value: value.clone(),
412 })
413 .collect(),
414 })
415 .collect();
416 reddb_file::encode_native_doc_pathvalue_frame(&reddb_file::NativeDocPathValueFrame {
417 collection: collection.to_string(),
418 docs,
419 })
420 }
421
422 pub(crate) fn inspect_native_document_pathvalue_artifact(
423 bytes: &[u8],
424 ) -> Result<(u64, u64, u64, u64), String> {
425 let frame =
426 reddb_file::decode_native_doc_pathvalue_frame(bytes).map_err(|e| e.to_string())?;
427 let doc_count = frame.docs.len() as u64;
428 let mut paths = BTreeSet::new();
429 let mut values = BTreeSet::new();
430 let mut total_entries = 0u64;
431 for doc in frame.docs {
432 for entry in doc.entries {
433 paths.insert(entry.path);
434 values.insert(entry.value);
435 total_entries += 1;
436 }
437 }
438 Ok((
439 doc_count,
440 paths.len() as u64,
441 total_entries,
442 values.len() as u64,
443 ))
444 }
445
446 fn native_document_pathvalue_for_entity(
447 entity_id: EntityId,
448 data: &EntityData,
449 ) -> Option<(EntityId, Vec<(String, String)>)> {
450 let mut entries = Vec::new();
451 match data {
452 EntityData::Row(row) => {
453 if let Some(named) = &row.named {
454 for (key, value) in named {
455 Self::collect_native_document_entries_from_value(key, value, &mut entries);
456 }
457 }
458 for (idx, value) in row.columns.iter().enumerate() {
459 let path = format!("columns[{idx}]");
460 Self::collect_native_document_entries_from_value(&path, value, &mut entries);
461 }
462 }
463 EntityData::Node(node) => {
464 for (key, value) in &node.properties {
465 Self::collect_native_document_entries_from_value(key, value, &mut entries);
466 }
467 }
468 EntityData::Edge(edge) => {
469 for (key, value) in &edge.properties {
470 Self::collect_native_document_entries_from_value(key, value, &mut entries);
471 }
472 }
473 EntityData::Vector(_) => {}
474 EntityData::TimeSeries(_) => {}
475 EntityData::QueueMessage(_) => {}
476 }
477 if entries.is_empty() {
478 None
479 } else {
480 Some((entity_id, entries))
481 }
482 }
483
484 fn collect_native_document_entries_from_value(
485 path: &str,
486 value: &Value,
487 out: &mut Vec<(String, String)>,
488 ) {
489 match value {
490 Value::Json(bytes) | Value::Blob(bytes) => {
491 if let Ok(json) = crate::json::from_slice::<JsonValue>(bytes) {
492 Self::collect_native_document_entries_from_json(path, &json, out);
493 }
494 }
495 _ => {}
496 }
497 }
498
499 fn collect_native_document_entries_from_json(
500 path: &str,
501 value: &JsonValue,
502 out: &mut Vec<(String, String)>,
503 ) {
504 match value {
505 JsonValue::Object(entries) => {
506 for (key, value) in entries {
507 let next = if path.is_empty() {
508 key.clone()
509 } else {
510 format!("{path}.{key}")
511 };
512 Self::collect_native_document_entries_from_json(&next, value, out);
513 }
514 }
515 JsonValue::Array(items) => {
516 for (idx, value) in items.iter().enumerate() {
517 let next = format!("{path}[{idx}]");
518 Self::collect_native_document_entries_from_json(&next, value, out);
519 }
520 }
521 _ => {
522 if let Some(text) = Self::native_json_scalar_text(value) {
523 out.push((path.to_string(), text));
524 }
525 }
526 }
527 }
528
529 fn native_json_scalar_text(value: &JsonValue) -> Option<String> {
530 match value {
531 JsonValue::Null => None,
532 JsonValue::Bool(value) => Some(value.to_string()),
533 JsonValue::Integer(value) => Some(value.to_string()),
534 JsonValue::Number(value) => Some(value.to_string()),
535 JsonValue::Decimal(value) => Some(value.clone()),
536 JsonValue::String(value) => Some(value.clone()),
537 JsonValue::Array(_) | JsonValue::Object(_) => None,
538 }
539 }
540
541 fn native_fulltext_text_for_entity(data: &EntityData) -> String {
542 match data {
543 EntityData::Row(row) => {
544 let mut parts = Vec::new();
545 if let Some(named) = &row.named {
546 for value in named.values() {
547 if let Some(text) = Self::native_value_text(value) {
548 parts.push(text);
549 }
550 }
551 }
552 for value in &row.columns {
553 if let Some(text) = Self::native_value_text(value) {
554 parts.push(text);
555 }
556 }
557 parts.join(" ")
558 }
559 EntityData::Node(node) => node
560 .properties
561 .values()
562 .filter_map(Self::native_value_text)
563 .collect::<Vec<_>>()
564 .join(" "),
565 EntityData::Edge(edge) => edge
566 .properties
567 .values()
568 .filter_map(Self::native_value_text)
569 .collect::<Vec<_>>()
570 .join(" "),
571 EntityData::Vector(vector) => vector.content.clone().unwrap_or_default(),
572 EntityData::TimeSeries(ts) => ts.metric.clone(),
573 EntityData::QueueMessage(_) => String::new(),
574 }
575 }
576
577 fn native_value_text(value: &Value) -> Option<String> {
578 match value {
579 Value::Text(value) => Some(value.to_string()),
580 Value::Json(value) => String::from_utf8(value.clone()).ok(),
581 Value::Blob(value) => String::from_utf8(value.clone()).ok(),
582 Value::Integer(value) => Some(value.to_string()),
583 Value::UnsignedInteger(value) => Some(value.to_string()),
584 Value::Float(value) => Some(value.to_string()),
585 Value::Boolean(value) => Some(value.to_string()),
586 Value::IpAddr(value) => Some(value.to_string()),
587 Value::NodeRef(value) => Some(value.clone()),
588 Value::EdgeRef(value) => Some(value.clone()),
589 Value::RowRef(table, row_id) => Some(format!("{table}:{row_id}")),
590 Value::VectorRef(collection, vector_id) => Some(format!("{collection}:{vector_id}")),
591 Value::Timestamp(value) => Some(value.to_string()),
592 Value::Duration(value) => Some(value.to_string()),
593 Value::Uuid(_) | Value::MacAddr(_) | Value::Vector(_) | Value::Null => None,
594 Value::Color([r, g, b]) => Some(format!("#{:02X}{:02X}{:02X}", r, g, b)),
595 Value::Email(s) => Some(s.clone()),
596 Value::Url(s) => Some(s.clone()),
597 Value::Phone(n) => Some(format!("+{}", n)),
598 Value::Semver(packed) => Some(format!(
599 "{}.{}.{}",
600 packed / 1_000_000,
601 (packed / 1_000) % 1_000,
602 packed % 1_000
603 )),
604 Value::Cidr(ip, prefix) => Some(format!(
605 "{}.{}.{}.{}/{}",
606 (ip >> 24) & 0xFF,
607 (ip >> 16) & 0xFF,
608 (ip >> 8) & 0xFF,
609 ip & 0xFF,
610 prefix
611 )),
612 Value::Date(days) => Some(days.to_string()),
613 Value::Time(ms) => {
614 let total_secs = ms / 1000;
615 Some(format!(
616 "{:02}:{:02}:{:02}",
617 total_secs / 3600,
618 (total_secs / 60) % 60,
619 total_secs % 60
620 ))
621 }
622 Value::Decimal(v) => Some(Value::Decimal(*v).display_string()),
623 Value::DecimalText(v) => Some(v.clone()),
624 Value::EnumValue(i) => Some(format!("enum({})", i)),
625 Value::Array(_) => None,
626 Value::TimestampMs(ms) => Some(ms.to_string()),
627 Value::Ipv4(ip) => Some(format!(
628 "{}.{}.{}.{}",
629 (ip >> 24) & 0xFF,
630 (ip >> 16) & 0xFF,
631 (ip >> 8) & 0xFF,
632 ip & 0xFF
633 )),
634 Value::Ipv6(bytes) => Some(format!("{}", std::net::Ipv6Addr::from(*bytes))),
635 Value::Subnet(ip, mask) => {
636 let prefix = mask.leading_ones();
637 Some(format!(
638 "{}.{}.{}.{}/{}",
639 (ip >> 24) & 0xFF,
640 (ip >> 16) & 0xFF,
641 (ip >> 8) & 0xFF,
642 ip & 0xFF,
643 prefix
644 ))
645 }
646 Value::Port(p) => Some(p.to_string()),
647 Value::Latitude(micro) => Some(format!("{:.6}", *micro as f64 / 1_000_000.0)),
648 Value::Longitude(micro) => Some(format!("{:.6}", *micro as f64 / 1_000_000.0)),
649 Value::GeoPoint(lat, lon) => Some(format!(
650 "{:.6},{:.6}",
651 *lat as f64 / 1_000_000.0,
652 *lon as f64 / 1_000_000.0
653 )),
654 Value::Country2(c) => Some(String::from_utf8_lossy(c).to_string()),
655 Value::Country3(c) => Some(String::from_utf8_lossy(c).to_string()),
656 Value::Lang2(c) => Some(String::from_utf8_lossy(c).to_string()),
657 Value::Lang5(c) => Some(String::from_utf8_lossy(c).to_string()),
658 Value::Currency(c) => Some(String::from_utf8_lossy(c).to_string()),
659 Value::AssetCode(code) => Some(code.clone()),
660 Value::Money { .. } => Some(value.display_string()),
661 Value::ColorAlpha([r, g, b, a]) => {
662 Some(format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a))
663 }
664 Value::BigInt(v) => Some(v.to_string()),
665 Value::KeyRef(col, key) => Some(format!("{}:{}", col, key)),
666 Value::DocRef(col, id) => Some(format!("{}#{}", col, id)),
667 Value::TableRef(name) => Some(name.clone()),
668 Value::PageRef(page_id) => Some(format!("page:{}", page_id)),
669 Value::Secret(_) | Value::Password(_) => None,
670 }
671 }
672
673 fn native_fulltext_tokenize(text: &str) -> Vec<String> {
674 text.to_lowercase()
675 .split(|c: char| !c.is_alphanumeric())
676 .filter(|s| s.len() >= 2)
677 .map(|s| s.to_string())
678 .collect()
679 }
680
681 pub(crate) fn native_recovery_summary_from_metadata(
682 metadata: &PhysicalMetadataFile,
683 ) -> NativeRecoverySummary {
684 const SAMPLE_LIMIT: usize = 16;
685
686 let snapshots: Vec<_> = metadata
687 .snapshots
688 .iter()
689 .rev()
690 .take(SAMPLE_LIMIT)
691 .map(|snapshot| NativeSnapshotSummary {
692 snapshot_id: snapshot.snapshot_id,
693 created_at_unix_ms: snapshot.created_at_unix_ms,
694 superblock_sequence: snapshot.superblock_sequence,
695 collection_count: snapshot.collection_count as u32,
696 total_entities: snapshot.total_entities as u64,
697 })
698 .collect();
699 let exports: Vec<_> = metadata
700 .exports
701 .iter()
702 .rev()
703 .take(SAMPLE_LIMIT)
704 .map(|export| NativeExportSummary {
705 name: export.name.clone(),
706 created_at_unix_ms: export.created_at_unix_ms,
707 snapshot_id: export.snapshot_id,
708 superblock_sequence: export.superblock_sequence,
709 collection_count: export.collection_count as u32,
710 total_entities: export.total_entities as u64,
711 })
712 .collect();
713
714 NativeRecoverySummary {
715 snapshot_count: metadata.snapshots.len() as u32,
716 export_count: metadata.exports.len() as u32,
717 snapshots_complete: metadata.snapshots.len() <= SAMPLE_LIMIT,
718 exports_complete: metadata.exports.len() <= SAMPLE_LIMIT,
719 omitted_snapshot_count: metadata.snapshots.len().saturating_sub(snapshots.len()) as u32,
720 omitted_export_count: metadata.exports.len().saturating_sub(exports.len()) as u32,
721 snapshots,
722 exports,
723 }
724 }
725
726 pub(crate) fn native_catalog_summary_from_metadata(
727 metadata: &PhysicalMetadataFile,
728 ) -> NativeCatalogSummary {
729 const SAMPLE_LIMIT: usize = 32;
730
731 let collections: Vec<_> = metadata
732 .catalog
733 .stats_by_collection
734 .iter()
735 .take(SAMPLE_LIMIT)
736 .map(|(name, stats)| NativeCatalogCollectionSummary {
737 name: name.clone(),
738 entities: stats.entities as u64,
739 cross_refs: stats.cross_refs as u64,
740 segments: stats.segments as u32,
741 })
742 .collect();
743
744 NativeCatalogSummary {
745 collection_count: metadata.catalog.total_collections as u32,
746 total_entities: metadata.catalog.total_entities as u64,
747 collections_complete: metadata.catalog.stats_by_collection.len() <= SAMPLE_LIMIT,
748 omitted_collection_count: metadata
749 .catalog
750 .stats_by_collection
751 .len()
752 .saturating_sub(collections.len()) as u32,
753 collections,
754 }
755 }
756
757 pub(crate) fn native_metadata_state_summary_from_metadata(
758 metadata: &PhysicalMetadataFile,
759 ) -> NativeMetadataStateSummary {
760 NativeMetadataStateSummary {
761 protocol_version: metadata.protocol_version.clone(),
762 generated_at_unix_ms: metadata.generated_at_unix_ms,
763 last_loaded_from: metadata.last_loaded_from.clone(),
764 last_healed_at_unix_ms: metadata.last_healed_at_unix_ms,
765 }
766 }
767
768 pub(crate) fn inspect_native_header_against_metadata(
769 native: PhysicalFileHeader,
770 metadata: &PhysicalMetadataFile,
771 ) -> NativeHeaderInspection {
772 let expected = Self::native_header_from_metadata(metadata);
773 let mut mismatches = Vec::new();
774
775 if native.format_version != expected.format_version {
776 mismatches.push(NativeHeaderMismatch {
777 field: "format_version",
778 native: native.format_version.to_string(),
779 expected: expected.format_version.to_string(),
780 });
781 }
782 if native.sequence != expected.sequence {
783 mismatches.push(NativeHeaderMismatch {
784 field: "sequence",
785 native: native.sequence.to_string(),
786 expected: expected.sequence.to_string(),
787 });
788 }
789 if native.manifest_oldest_root != expected.manifest_oldest_root {
790 mismatches.push(NativeHeaderMismatch {
791 field: "manifest_oldest_root",
792 native: native.manifest_oldest_root.to_string(),
793 expected: expected.manifest_oldest_root.to_string(),
794 });
795 }
796 if native.manifest_root != expected.manifest_root {
797 mismatches.push(NativeHeaderMismatch {
798 field: "manifest_root",
799 native: native.manifest_root.to_string(),
800 expected: expected.manifest_root.to_string(),
801 });
802 }
803 if native.free_set_root != expected.free_set_root {
804 mismatches.push(NativeHeaderMismatch {
805 field: "free_set_root",
806 native: native.free_set_root.to_string(),
807 expected: expected.free_set_root.to_string(),
808 });
809 }
810 if native.collection_root_count != expected.collection_root_count {
811 mismatches.push(NativeHeaderMismatch {
812 field: "collection_root_count",
813 native: native.collection_root_count.to_string(),
814 expected: expected.collection_root_count.to_string(),
815 });
816 }
817 if native.snapshot_count != expected.snapshot_count {
818 mismatches.push(NativeHeaderMismatch {
819 field: "snapshot_count",
820 native: native.snapshot_count.to_string(),
821 expected: expected.snapshot_count.to_string(),
822 });
823 }
824 if native.index_count != expected.index_count {
825 mismatches.push(NativeHeaderMismatch {
826 field: "index_count",
827 native: native.index_count.to_string(),
828 expected: expected.index_count.to_string(),
829 });
830 }
831 if native.catalog_collection_count != expected.catalog_collection_count {
832 mismatches.push(NativeHeaderMismatch {
833 field: "catalog_collection_count",
834 native: native.catalog_collection_count.to_string(),
835 expected: expected.catalog_collection_count.to_string(),
836 });
837 }
838 if native.catalog_total_entities != expected.catalog_total_entities {
839 mismatches.push(NativeHeaderMismatch {
840 field: "catalog_total_entities",
841 native: native.catalog_total_entities.to_string(),
842 expected: expected.catalog_total_entities.to_string(),
843 });
844 }
845 if native.export_count != expected.export_count {
846 mismatches.push(NativeHeaderMismatch {
847 field: "export_count",
848 native: native.export_count.to_string(),
849 expected: expected.export_count.to_string(),
850 });
851 }
852 if native.graph_projection_count != expected.graph_projection_count {
853 mismatches.push(NativeHeaderMismatch {
854 field: "graph_projection_count",
855 native: native.graph_projection_count.to_string(),
856 expected: expected.graph_projection_count.to_string(),
857 });
858 }
859 if native.analytics_job_count != expected.analytics_job_count {
860 mismatches.push(NativeHeaderMismatch {
861 field: "analytics_job_count",
862 native: native.analytics_job_count.to_string(),
863 expected: expected.analytics_job_count.to_string(),
864 });
865 }
866 if native.manifest_event_count != expected.manifest_event_count {
867 mismatches.push(NativeHeaderMismatch {
868 field: "manifest_event_count",
869 native: native.manifest_event_count.to_string(),
870 expected: expected.manifest_event_count.to_string(),
871 });
872 }
873
874 NativeHeaderInspection {
875 native,
876 expected,
877 consistent: mismatches.is_empty(),
878 mismatches,
879 }
880 }
881
882 pub(crate) fn repair_policy_for_inspection(
883 inspection: &NativeHeaderInspection,
884 ) -> NativeHeaderRepairPolicy {
885 if inspection.consistent {
886 return NativeHeaderRepairPolicy::InSync;
887 }
888
889 if inspection.expected.sequence >= inspection.native.sequence {
890 NativeHeaderRepairPolicy::RepairNativeFromMetadata
891 } else {
892 NativeHeaderRepairPolicy::NativeAheadOfMetadata
893 }
894 }
895
896 pub(crate) fn prune_export_registry(&self, exports: &mut Vec<ExportDescriptor>) {
897 let retention = self.options.export_retention.max(1);
898 if exports.len() <= retention {
899 return;
900 }
901
902 exports.sort_by_key(|export| export.created_at_unix_ms);
903 let removed: Vec<ExportDescriptor> =
904 exports.drain(0..(exports.len() - retention)).collect();
905
906 for export in removed {
907 let _ = fs::remove_file(&export.data_path);
908 let _ = fs::remove_file(&export.metadata_path);
909 let binary_path = PhysicalMetadataFile::metadata_binary_path_for(std::path::Path::new(
910 &export.data_path,
911 ));
912 let _ = fs::remove_file(binary_path);
913 }
914 }
915
916 pub(crate) fn runtime_index_catalog(&self) -> IndexCatalog {
917 let mut catalog = IndexCatalog::register_default_vector_graph(
918 self.options.has_capability(Capability::Table),
919 self.options.has_capability(Capability::Graph),
920 );
921 if self.options.has_capability(Capability::FullText) {
922 catalog.register(RuntimeIndexConfig::new(
923 "text-fulltext",
924 IndexKind::FullText,
925 ));
926 catalog.register(RuntimeIndexConfig::new(
927 "document-pathvalue",
928 IndexKind::DocumentPathValue,
929 ));
930 }
931 catalog.register(RuntimeIndexConfig::new(
932 "search-hybrid",
933 IndexKind::HybridSearch,
934 ));
935 catalog
936 }
937
938 pub(crate) fn physical_index_state(&self) -> Vec<PhysicalIndexState> {
939 let catalog = self.runtime_index_catalog();
943 let snapshot = crate::catalog::snapshot_store_with_declarations(
944 "reddb",
945 self.store.as_ref(),
946 Some(&catalog),
947 None, None, );
950 let mut metrics_by_name = std::collections::BTreeMap::new();
951 for metric in &snapshot.indices {
952 metrics_by_name.insert(metric.name.clone(), metric.clone());
953 }
954
955 let mut states = Vec::new();
956 for collection in snapshot.collections {
957 for index_name in &collection.indices {
958 let metric = metrics_by_name.get(index_name);
959 let kind = metric
960 .map(|metric| metric.kind)
961 .unwrap_or_else(|| infer_collection_index_kind(collection.model, index_name));
962 let entries = estimate_index_entries(&collection, kind);
963 states.push(PhysicalIndexState {
964 name: format!("{}::{}", collection.name, index_name),
965 kind,
966 collection: Some(collection.name.clone()),
967 enabled: metric.map(|metric| metric.enabled).unwrap_or(true),
968 entries,
969 estimated_memory_bytes: estimate_index_memory(entries, kind),
970 last_refresh_ms: metric.and_then(|metric| metric.last_refresh_ms),
971 backend: index_backend_name(kind).to_string(),
972 artifact_kind: None,
973 artifact_root_page: None,
974 artifact_checksum: None,
975 build_state: "catalog-derived".to_string(),
976 });
977 }
978 }
979
980 states
981 }
982
983 pub(crate) fn physical_collection_roots(&self) -> BTreeMap<String, u64> {
984 let mut roots = BTreeMap::new();
985
986 for name in self.store.list_collections() {
987 let Some(manager) = self.store.get_collection(&name) else {
988 continue;
989 };
990
991 let stats = manager.stats();
992 let mut root = fnv1a_seed();
993 fnv1a_hash_value(&mut root, &name);
994 fnv1a_hash_value(&mut root, &stats.total_entities);
995 fnv1a_hash_value(&mut root, &stats.growing_count);
996 fnv1a_hash_value(&mut root, &stats.sealed_count);
997 fnv1a_hash_value(&mut root, &stats.archived_count);
998 fnv1a_hash_value(&mut root, &stats.total_memory_bytes);
999 fnv1a_hash_value(&mut root, &stats.seal_ops);
1000 fnv1a_hash_value(&mut root, &stats.consolidation.runs_completed);
1001
1002 let mut entities = manager.query_all(|_| true);
1003 entities.sort_by_key(|entity| entity.id.raw());
1004
1005 for entity in entities {
1006 fnv1a_hash_value(&mut root, &entity.id.raw());
1007 fnv1a_hash_value(&mut root, &entity.kind);
1008 fnv1a_hash_value(&mut root, &entity.created_at);
1009 fnv1a_hash_value(&mut root, &entity.updated_at);
1010 fnv1a_hash_value(&mut root, &entity.data);
1011 fnv1a_hash_value(&mut root, &entity.sequence_id);
1012 fnv1a_hash_value(&mut root, &entity.embeddings().len());
1013 fnv1a_hash_value(&mut root, &entity.cross_refs().len());
1014 }
1015
1016 roots.insert(name, root);
1017 }
1018
1019 roots
1020 }
1021
1022 pub fn table_ref(&self, table: impl Into<String>, row_id: u64) -> TableRef {
1028 TableRef::new(table, row_id)
1029 }
1030
1031 pub fn node_ref(&self, collection: impl Into<String>, node_id: EntityId) -> NodeRef {
1033 NodeRef::new(collection, node_id)
1034 }
1035
1036 pub fn vector_ref(&self, collection: impl Into<String>, vector_id: EntityId) -> VectorRef {
1038 VectorRef::new(collection, vector_id)
1039 }
1040
1041 pub fn query(&self) -> QueryBuilder {
1047 QueryBuilder::new(self.store.clone())
1048 }
1049
1050 pub fn similar(&self, collection: &str, vector: &[f32], k: usize) -> Vec<SimilarResult> {
1057 if self.store.get_collection(collection).is_none() {
1058 return Vec::new();
1059 }
1060
1061 if let Some(index) = self.get_or_build_hnsw_index(collection, vector.len()) {
1063 let hnsw = index.read().unwrap_or_else(|e| e.into_inner());
1064 let results = hnsw.search(vector, k);
1065 let mapped = self.hnsw_results_to_similar(collection, &results);
1066 if !mapped.is_empty() {
1067 return mapped;
1068 }
1069 }
1070
1071 self.similar_brute_force(collection, vector, k)
1073 }
1074
1075 fn similar_brute_force(
1077 &self,
1078 collection: &str,
1079 vector: &[f32],
1080 k: usize,
1081 ) -> Vec<SimilarResult> {
1082 let manager = match self.store.get_collection(collection) {
1083 Some(m) => m,
1084 None => return Vec::new(),
1085 };
1086
1087 let entities = manager.query_all(|_| true);
1088 let mut results: Vec<SimilarResult> = entities
1089 .iter()
1090 .filter_map(|e| {
1091 let score = match &e.data {
1092 EntityData::Vector(v) => cosine_similarity(vector, &v.dense),
1093 _ => e
1094 .embeddings()
1095 .iter()
1096 .map(|emb| cosine_similarity(vector, &emb.vector))
1097 .fold(0.0f32, f32::max),
1098 };
1099 let distance = (1.0 - score).max(0.0);
1100 if score > 0.0 {
1101 Some(SimilarResult {
1102 entity_id: e.id,
1103 score,
1104 distance,
1105 entity: e.clone(),
1106 })
1107 } else {
1108 None
1109 }
1110 })
1111 .collect();
1112
1113 results.sort_by(|a, b| {
1114 b.score
1115 .partial_cmp(&a.score)
1116 .unwrap_or(std::cmp::Ordering::Equal)
1117 });
1118 results.truncate(k);
1119 results
1120 }
1121
1122 fn get_or_build_hnsw_index(
1132 &self,
1133 collection: &str,
1134 query_dim: usize,
1135 ) -> Option<Arc<RwLock<HnswIndex>>> {
1136 let manager = self.store.get_collection(collection)?;
1137 let live_count = manager.count();
1138
1139 {
1141 let indexes = self
1142 .vector_indexes
1143 .read()
1144 .unwrap_or_else(|e| e.into_inner());
1145 if let Some(cached) = indexes.get(collection) {
1146 if cached.entity_count == live_count {
1147 return Some(Arc::clone(&cached.index));
1148 }
1149 }
1150 }
1151
1152 let entities = manager.query_all(|_| true);
1154
1155 let vectors: Vec<(u64, Vec<f32>)> = entities
1156 .iter()
1157 .filter_map(|e| match &e.data {
1158 EntityData::Vector(v) if !v.dense.is_empty() && v.dense.len() == query_dim => {
1159 Some((e.id.raw(), v.dense.clone()))
1160 }
1161 _ => None,
1162 })
1163 .collect();
1164
1165 const MIN_VECTORS_FOR_HNSW: usize = 100;
1167 if vectors.len() < MIN_VECTORS_FOR_HNSW {
1168 return None;
1169 }
1170
1171 let config = crate::storage::engine::HnswConfig::with_m(16)
1173 .with_metric(crate::storage::engine::DistanceMetric::Cosine)
1174 .with_ef_construction(100)
1175 .with_ef_search(50);
1176 let mut hnsw = HnswIndex::new(query_dim, config);
1177
1178 for (id, vec) in &vectors {
1179 hnsw.insert_with_id(*id, vec.clone());
1180 }
1181
1182 let index = Arc::new(RwLock::new(hnsw));
1183
1184 let mut indexes = self
1186 .vector_indexes
1187 .write()
1188 .unwrap_or_else(|e| e.into_inner());
1189 if let Some(cached) = indexes.get(collection) {
1191 if cached.entity_count == live_count {
1192 return Some(Arc::clone(&cached.index));
1193 }
1194 }
1195 indexes.insert(
1196 collection.to_string(),
1197 CachedVectorIndex {
1198 index: Arc::clone(&index),
1199 entity_count: live_count,
1200 },
1201 );
1202 Some(index)
1203 }
1204
1205 fn hnsw_results_to_similar(
1207 &self,
1208 collection: &str,
1209 results: &[crate::storage::engine::DistanceResult],
1210 ) -> Vec<SimilarResult> {
1211 results
1212 .iter()
1213 .filter_map(|dr| {
1214 let entity_id = EntityId::new(dr.id);
1215 let entity = self.store.get(collection, entity_id)?;
1216 let score = (1.0 - dr.distance).max(0.0);
1218 if score > 0.0 {
1219 Some(SimilarResult {
1220 entity_id,
1221 score,
1222 distance: dr.distance,
1223 entity,
1224 })
1225 } else {
1226 None
1227 }
1228 })
1229 .collect()
1230 }
1231
1232 pub(crate) fn invalidate_vector_index(&self, collection: &str) {
1237 let mut indexes = self
1238 .vector_indexes
1239 .write()
1240 .unwrap_or_else(|e| e.into_inner());
1241 indexes.remove(collection);
1242 }
1243
1244 pub fn get(&self, id: EntityId) -> Option<UnifiedEntity> {
1246 self.store.get_any(id).map(|(_, e)| e)
1247 }
1248
1249 pub fn get_with_collection(&self, id: EntityId) -> Option<(String, UnifiedEntity)> {
1251 self.store.get_any(id)
1252 }
1253
1254 pub fn batch_get(&self, ids: &[EntityId]) -> Vec<Option<UnifiedEntity>> {
1260 ids.iter().map(|id| self.get(*id)).collect()
1261 }
1262
1263 pub fn batch(&self) -> BatchBuilder {
1265 BatchBuilder::new(self.store.clone(), self.preprocessors.clone())
1266 }
1267
1268 pub fn add_preprocessor(&mut self, preprocessor: Box<dyn Preprocessor>) {
1274 let mut preprocessors = self
1275 .preprocessors
1276 .write()
1277 .unwrap_or_else(|poisoned| poisoned.into_inner());
1278 preprocessors.push(Arc::from(preprocessor));
1279 }
1280
1281 pub fn linked_from(&self, id: EntityId) -> Vec<LinkedEntity> {
1287 self.store
1288 .get_refs_from(id)
1289 .into_iter()
1290 .filter_map(|(target_id, ref_type, collection)| {
1291 self.store
1292 .get(&collection, target_id)
1293 .map(|entity| LinkedEntity {
1294 entity,
1295 ref_type,
1296 collection,
1297 })
1298 })
1299 .collect()
1300 }
1301
1302 pub fn linked_to(&self, id: EntityId) -> Vec<LinkedEntity> {
1304 self.store
1305 .get_refs_to(id)
1306 .into_iter()
1307 .filter_map(|(source_id, ref_type, collection)| {
1308 self.store
1309 .get(&collection, source_id)
1310 .map(|entity| LinkedEntity {
1311 entity,
1312 ref_type,
1313 collection,
1314 })
1315 })
1316 .collect()
1317 }
1318
1319 pub fn store(&self) -> Arc<UnifiedStore> {
1321 self.store.clone()
1322 }
1323
1324 pub(crate) fn turbo_collections(
1330 &self,
1331 ) -> &Arc<
1332 parking_lot::Mutex<
1333 std::collections::HashMap<
1334 String,
1335 Arc<crate::runtime::vector_turbo_kind::TurboCollectionState>,
1336 >,
1337 >,
1338 > {
1339 self.turbo_collections
1340 .get_or_init(|| Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new())))
1341 }
1342
1343 pub(crate) fn turbo_state(
1353 &self,
1354 collection: &str,
1355 ) -> Option<Arc<crate::runtime::vector_turbo_kind::TurboCollectionState>> {
1356 if !crate::runtime::vector_turbo_kind::is_turbo(&self.store, collection) {
1357 return None;
1358 }
1359 let map = self.turbo_collections();
1360 {
1361 let guard = map.lock();
1362 if let Some(state) = guard.get(collection) {
1363 return Some(Arc::clone(state));
1364 }
1365 }
1366 let contract = self.collection_contract(collection)?;
1367 let dim = contract.vector_dimension?;
1368 let metric = contract
1369 .vector_metric
1370 .unwrap_or(crate::storage::engine::distance::DistanceMetric::Cosine);
1371 let state = Arc::new(
1372 crate::runtime::vector_turbo_kind::TurboCollectionState::new(
1373 dim,
1374 metric,
1375 self.store.pager(),
1376 ),
1377 );
1378 if let Some((_, paths)) = self.options.resolve_tiered_layout() {
1383 state.set_snapshot_path(paths.turbo_snapshot_path(collection));
1384 }
1385 let mut guard = map.lock();
1386 let inserted_now = !guard.contains_key(collection);
1387 let entry = guard
1388 .entry(collection.to_string())
1389 .or_insert_with(|| Arc::clone(&state));
1390 let handle = Arc::clone(entry);
1391 drop(guard);
1392 if inserted_now {
1399 let join = crate::runtime::vector_turbo_kind::spawn_background_rebuild(
1400 Arc::clone(&self.store),
1401 collection.to_string(),
1402 Arc::clone(&handle),
1403 );
1404 self.turbo_rebuild_workers.lock().push(join);
1405 }
1406 Some(handle)
1407 }
1408
1409 pub fn ml_runtime(&self) -> &crate::storage::ml::MlRuntime {
1415 self.ml_runtime.get_or_init(|| {
1416 crate::storage::ml::MlRuntime::in_memory(std::sync::Arc::new(
1417 |_handle| Ok(String::new()),
1421 ))
1422 })
1423 }
1424
1425 pub fn semantic_cache(&self) -> &Arc<crate::storage::ml::SemanticCache> {
1429 self.semantic_cache.get_or_init(|| {
1430 Arc::new(crate::storage::ml::SemanticCache::new(
1431 crate::storage::ml::SemanticCacheConfig::default(),
1432 ))
1433 })
1434 }
1435
1436 pub fn hypertables(&self) -> &Arc<crate::storage::timeseries::HypertableRegistry> {
1439 self.hypertables
1440 .get_or_init(|| Arc::new(crate::storage::timeseries::HypertableRegistry::new()))
1441 }
1442
1443 pub fn continuous_aggregates(
1446 &self,
1447 ) -> &Arc<crate::storage::timeseries::continuous_aggregate::ContinuousAggregateEngine> {
1448 self.continuous_aggregates.get_or_init(|| {
1449 Arc::new(
1450 crate::storage::timeseries::continuous_aggregate::ContinuousAggregateEngine::new(),
1451 )
1452 })
1453 }
1454
1455 pub(crate) fn is_binary_dump(path: &Path) -> Result<bool, std::io::Error> {
1456 let mut file = File::open(path)?;
1457 let mut magic = [0u8; 4];
1458 let read = file.read(&mut magic)?;
1459 Ok(read == 4 && reddb_file::native_store_magic_matches(&magic))
1460 }
1461}