1use super::*;
2
3pub(crate) struct NativeArtifactBuild {
4 pub records: Vec<(NativeVectorArtifactSummary, Vec<u8>)>,
5 pub deferred_collections: std::collections::HashSet<String>,
6}
7
8impl RedDB {
9 pub fn quorum_coordinator(
16 &self,
17 ) -> Option<&Arc<crate::replication::quorum::QuorumCoordinator>> {
18 self.quorum.as_ref()
19 }
20
21 pub fn wait_for_replication_quorum(
32 &self,
33 target_lsn: u64,
34 ) -> Result<(), crate::replication::QuorumError> {
35 match &self.quorum {
36 Some(q) => q.wait_for_quorum(target_lsn),
37 None => Ok(()),
38 }
39 }
40}
41
42impl RedDB {
43 pub(crate) fn native_registry_summary_from_metadata(
44 &self,
45 metadata: &PhysicalMetadataFile,
46 artifact_summaries: &[NativeVectorArtifactSummary],
47 artifact_count: usize,
48 ) -> NativeRegistrySummary {
49 const SAMPLE_LIMIT: usize = 32;
50
51 let collection_names: Vec<_> = metadata
52 .catalog
53 .stats_by_collection
54 .keys()
55 .take(SAMPLE_LIMIT)
56 .cloned()
57 .collect();
58 let indexes: Vec<_> = metadata
59 .indexes
60 .iter()
61 .take(SAMPLE_LIMIT)
62 .map(|index| NativeRegistryIndexSummary {
63 name: index.name.clone(),
64 kind: index.kind.as_str().to_string(),
65 collection: index.collection.clone(),
66 enabled: index.enabled,
67 entries: index.entries as u64,
68 estimated_memory_bytes: index.estimated_memory_bytes,
69 last_refresh_ms: index.last_refresh_ms,
70 backend: index.backend.clone(),
71 })
72 .collect();
73 let graph_projections: Vec<_> = metadata
74 .graph_projections
75 .iter()
76 .take(SAMPLE_LIMIT)
77 .map(|projection| NativeRegistryProjectionSummary {
78 name: projection.name.clone(),
79 source: projection.source.clone(),
80 created_at_unix_ms: projection.created_at_unix_ms,
81 updated_at_unix_ms: projection.updated_at_unix_ms,
82 node_labels: projection.node_labels.clone(),
83 node_types: projection.node_types.clone(),
84 edge_labels: projection.edge_labels.clone(),
85 last_materialized_sequence: projection.last_materialized_sequence,
86 })
87 .collect();
88 let analytics_jobs = metadata
89 .analytics_jobs
90 .iter()
91 .take(SAMPLE_LIMIT)
92 .map(|job| NativeRegistryJobSummary {
93 id: job.id.clone(),
94 kind: job.kind.clone(),
95 projection: job.projection.clone(),
96 state: job.state.clone(),
97 created_at_unix_ms: job.created_at_unix_ms,
98 updated_at_unix_ms: job.updated_at_unix_ms,
99 last_run_sequence: job.last_run_sequence,
100 metadata: job.metadata.clone(),
101 })
102 .collect::<Vec<_>>();
103 let vector_artifacts = artifact_summaries
104 .iter()
105 .take(SAMPLE_LIMIT)
106 .cloned()
107 .collect::<Vec<_>>();
108 let vector_artifact_count = artifact_count as u32;
109
110 NativeRegistrySummary {
111 collection_count: metadata.catalog.total_collections as u32,
112 index_count: metadata.indexes.len() as u32,
113 graph_projection_count: metadata.graph_projections.len() as u32,
114 analytics_job_count: metadata.analytics_jobs.len() as u32,
115 vector_artifact_count,
116 collections_complete: metadata.catalog.stats_by_collection.len() <= SAMPLE_LIMIT,
117 indexes_complete: metadata.indexes.len() <= SAMPLE_LIMIT,
118 graph_projections_complete: metadata.graph_projections.len() <= SAMPLE_LIMIT,
119 analytics_jobs_complete: metadata.analytics_jobs.len() <= SAMPLE_LIMIT,
120 vector_artifacts_complete: vector_artifact_count as usize <= SAMPLE_LIMIT,
121 omitted_collection_count: metadata
122 .catalog
123 .stats_by_collection
124 .len()
125 .saturating_sub(collection_names.len())
126 as u32,
127 omitted_index_count: metadata.indexes.len().saturating_sub(indexes.len()) as u32,
128 omitted_graph_projection_count: metadata
129 .graph_projections
130 .len()
131 .saturating_sub(graph_projections.len())
132 as u32,
133 omitted_analytics_job_count: metadata
134 .analytics_jobs
135 .len()
136 .saturating_sub(analytics_jobs.len())
137 as u32,
138 omitted_vector_artifact_count: vector_artifact_count
139 .saturating_sub(vector_artifacts.len() as u32),
140 collection_names,
141 indexes,
142 graph_projections,
143 analytics_jobs,
144 vector_artifacts,
145 }
146 }
147
148 pub(crate) fn native_vector_artifact_records(
149 &self,
150 ) -> Result<NativeArtifactBuild, Box<dyn std::error::Error>> {
151 let budget = crate::storage::memory_budget::resolve_for_boot(
152 self.options.storage_profile.deploy_profile,
153 self.options.memory_budget_bytes,
154 )?
155 .resolved_bytes;
156 let source_limit = usize::try_from((budget / 256).clamp(1024, 2 * 1024 * 1024))
160 .expect("bounded source allowance");
161 let output_limit = usize::try_from((budget / 128).min(8 * 1024 * 1024))
162 .expect("bounded artifact allowance");
163 let mut artifacts = Vec::new();
164 let mut retained_bytes = 0usize;
165 let mut deferred_collections = std::collections::HashSet::new();
166 let mut collections = self.store.list_collections();
167 collections.sort_unstable();
168 'collections: for collection in collections {
169 let Some(manager) = self.store.get_collection(&collection) else {
170 continue;
171 };
172 if manager.count() == 0 {
173 continue;
174 }
175 let mut entities = Vec::new();
176 let mut source_bytes = 0usize;
177 let mut exceeded = false;
178 let column_schema = manager.column_schema();
179 manager.for_each_entity(|entity| {
180 let eligible = match &entity.data {
181 EntityData::Vector(vector) => {
182 self.options.has_capability(Capability::Vector) && !vector.dense.is_empty()
183 }
184 EntityData::Edge(_) => self.options.has_capability(Capability::Graph),
185 _ => self.options.has_capability(Capability::FullText),
186 };
187 if !eligible {
188 return true;
189 }
190 let bytes = reddb_engine::storage::memory_size::entity_bytes(entity);
191 if bytes > source_limit.saturating_sub(source_bytes)
192 || retained_bytes >= output_limit
193 {
194 exceeded = true;
195 return false;
196 }
197 source_bytes += bytes;
198 let mut owned = entity.clone();
199 if let EntityData::Row(row) = &mut owned.data {
200 if row.named.is_none() && row.schema.is_none() {
201 row.schema = column_schema.clone();
202 }
203 }
204 entities.push(owned);
205 true
206 });
207 if exceeded {
208 deferred_collections.insert(collection);
209 continue;
210 }
211 if entities.is_empty() {
212 continue;
213 }
214 let first_artifact = artifacts.len();
215 let mut vectors = Vec::new();
216 let mut graph_edges = Vec::new();
217 let mut fulltext_documents = Vec::new();
218 let mut document_records = Vec::new();
219 let mut document_bytes = 0usize;
220 for entity in entities {
221 match entity.data {
222 EntityData::Vector(vector) => {
223 if !vector.dense.is_empty() {
224 vectors.push((entity.id, vector.dense));
225 }
226 }
227 EntityData::Edge(edge) => {
228 if let EntityKind::GraphEdge(edge_kind) = entity.kind {
229 graph_edges.push((
230 entity.id,
231 edge_kind.from_node,
232 edge_kind.to_node,
233 edge_kind.label,
234 edge.weight,
235 ));
236 }
237 }
238 data => {
239 let text = Self::native_fulltext_text_for_entity(&data);
240 if !text.trim().is_empty() {
241 fulltext_documents.push((entity.id, text));
242 }
243 let document = match Self::native_document_pathvalue_for_entity(
244 entity.id,
245 &data,
246 source_limit.saturating_sub(document_bytes),
247 ) {
248 Ok(document) => document,
249 Err(()) => {
250 artifacts.truncate(first_artifact);
251 deferred_collections.insert(collection);
252 continue 'collections;
253 }
254 };
255 if let Some(document) = document {
256 document_bytes += document
257 .1
258 .iter()
259 .map(|(path, value)| {
260 path.len()
261 + value.len()
262 + std::mem::size_of::<(String, String)>()
263 })
264 .sum::<usize>();
265 document_records.push(document);
266 }
267 }
268 }
269 }
270 if !vectors.is_empty() {
271 let dimension = vectors[0].1.len();
272 let mut hnsw = HnswIndex::with_dimension(dimension);
273 for (id, vector) in vectors
274 .into_iter()
275 .filter(|(_, vector)| vector.len() == dimension)
276 {
277 hnsw.insert_with_id(id.raw(), vector);
278 }
279 let stats = hnsw.stats();
280 let bytes = hnsw.to_bytes();
281 let summary = NativeVectorArtifactSummary {
282 collection: collection.clone(),
283 artifact_kind: "hnsw".to_string(),
284 vector_count: stats.node_count as u64,
285 dimension: stats.dimension as u32,
286 max_layer: stats.max_layer as u32,
287 serialized_bytes: bytes.len() as u64,
288 checksum: crate::storage::engine::crc32(&bytes) as u64,
289 };
290 artifacts.push((summary, bytes));
291
292 let n_lists = ((stats.node_count as f64).sqrt().ceil() as usize).max(1);
293 let mut ivf = IvfIndex::new(IvfConfig::new(dimension, n_lists));
294 let training = manager
295 .query_all(|_| true)
296 .into_iter()
297 .filter_map(|entity| match entity.data {
298 EntityData::Vector(vector) if vector.dense.len() == dimension => {
299 Some(vector.dense)
300 }
301 _ => None,
302 })
303 .collect::<Vec<_>>();
304 ivf.train(&training);
305 let items = manager
306 .query_all(|_| true)
307 .into_iter()
308 .filter_map(|entity| match entity.data {
309 EntityData::Vector(vector) if vector.dense.len() == dimension => {
310 Some((entity.id.raw(), vector.dense))
311 }
312 _ => None,
313 })
314 .collect::<Vec<_>>();
315 ivf.add_batch_with_ids(items);
316 let ivf_stats = ivf.stats();
317 let ivf_bytes = ivf.to_bytes();
318 let ivf_summary = NativeVectorArtifactSummary {
319 collection: collection.clone(),
320 artifact_kind: "ivf".to_string(),
321 vector_count: ivf_stats.total_vectors as u64,
322 dimension: ivf_stats.dimension as u32,
323 max_layer: ivf_stats.n_lists as u32,
324 serialized_bytes: ivf_bytes.len() as u64,
325 checksum: crate::storage::engine::crc32(&ivf_bytes) as u64,
326 };
327 artifacts.push((ivf_summary, ivf_bytes));
328 }
329
330 if !graph_edges.is_empty() {
331 let bytes = Self::serialize_native_graph_adjacency_artifact(&graph_edges);
332 let (edge_count, node_count, label_count) =
333 Self::inspect_native_graph_adjacency_artifact(&bytes).unwrap_or((0, 0, 0));
334 let summary = NativeVectorArtifactSummary {
335 collection: collection.clone(),
336 artifact_kind: "graph.adjacency".to_string(),
337 vector_count: edge_count,
338 dimension: node_count as u32,
339 max_layer: label_count,
340 serialized_bytes: bytes.len() as u64,
341 checksum: crate::storage::engine::crc32(&bytes) as u64,
342 };
343 artifacts.push((summary, bytes));
344 }
345
346 if !fulltext_documents.is_empty() {
347 let bytes =
348 Self::serialize_native_fulltext_artifact(&collection, &fulltext_documents);
349 let (doc_count, term_count, posting_count) =
350 Self::inspect_native_fulltext_artifact(&bytes).unwrap_or((0, 0, 0));
351 let summary = NativeVectorArtifactSummary {
352 collection: collection.clone(),
353 artifact_kind: "text.fulltext".to_string(),
354 vector_count: posting_count,
355 dimension: doc_count as u32,
356 max_layer: term_count as u32,
357 serialized_bytes: bytes.len() as u64,
358 checksum: crate::storage::engine::crc32(&bytes) as u64,
359 };
360 artifacts.push((summary, bytes));
361 }
362
363 if !document_records.is_empty() {
364 let bytes = Self::serialize_native_document_pathvalue_artifact(
365 &collection,
366 &document_records,
367 );
368 let (doc_count, path_count, value_count, unique_value_count) =
369 Self::inspect_native_document_pathvalue_artifact(&bytes)
370 .unwrap_or((0, 0, 0, 0));
371 let _ = unique_value_count;
372 let summary = NativeVectorArtifactSummary {
373 collection: collection.clone(),
374 artifact_kind: "document.pathvalue".to_string(),
375 vector_count: value_count,
376 dimension: doc_count as u32,
377 max_layer: path_count as u32,
378 serialized_bytes: bytes.len() as u64,
379 checksum: crate::storage::engine::crc32(&bytes) as u64,
380 };
381 artifacts.push((summary, bytes));
382 }
383 let bytes = artifacts[first_artifact..]
384 .iter()
385 .map(|(_, bytes)| bytes.len())
386 .sum::<usize>();
387 if bytes > output_limit.saturating_sub(retained_bytes) {
388 artifacts.truncate(first_artifact);
389 deferred_collections.insert(collection);
390 } else {
391 retained_bytes += bytes;
392 }
393 }
394 for collection in &deferred_collections {
395 tracing::warn!(collection, source_bytes_limit=source_limit, retained_bytes_limit=output_limit,
396 "native search artifact deferred by memory budget; canonical records remain available");
397 }
398 Ok(NativeArtifactBuild {
399 records: artifacts,
400 deferred_collections,
401 })
402 }
403
404 fn serialize_native_graph_adjacency_artifact(
405 edges: &[(EntityId, String, String, String, f32)],
406 ) -> Vec<u8> {
407 let edges: Vec<reddb_file::NativeGraphEdge> = edges
408 .iter()
409 .map(
410 |(edge_id, from_node, to_node, label, weight)| reddb_file::NativeGraphEdge {
411 edge_id: edge_id.raw(),
412 from_node: from_node.clone(),
413 to_node: to_node.clone(),
414 label: label.clone(),
415 weight: *weight,
416 },
417 )
418 .collect();
419 reddb_file::encode_native_graph_adjacency_frame(&reddb_file::NativeGraphAdjacencyFrame {
420 edges,
421 })
422 }
423
424 pub(crate) fn inspect_native_graph_adjacency_artifact(
425 bytes: &[u8],
426 ) -> Result<(u64, u64, u32), String> {
427 let frame =
428 reddb_file::decode_native_graph_adjacency_frame(bytes).map_err(|e| e.to_string())?;
429 let edge_count = frame.edges.len() as u64;
430 let mut nodes = BTreeSet::new();
431 let mut labels = BTreeSet::new();
432 for edge in frame.edges {
433 nodes.insert(edge.from_node);
434 nodes.insert(edge.to_node);
435 labels.insert(edge.label);
436 }
437 Ok((edge_count, nodes.len() as u64, labels.len() as u32))
438 }
439
440 fn serialize_native_fulltext_artifact(
441 collection: &str,
442 documents: &[(EntityId, String)],
443 ) -> Vec<u8> {
444 let mut postings: BTreeMap<String, Vec<(u64, u32)>> = BTreeMap::new();
445 for (entity_id, text) in documents {
446 let mut frequencies: BTreeMap<String, u32> = BTreeMap::new();
447 for token in Self::native_fulltext_tokenize(text) {
448 *frequencies.entry(token).or_insert(0) += 1;
449 }
450 for (token, count) in frequencies {
451 postings
452 .entry(token)
453 .or_default()
454 .push((entity_id.raw(), count));
455 }
456 }
457
458 let terms = postings
459 .into_iter()
460 .map(|(term, postings)| reddb_file::NativeFulltextTerm {
461 term,
462 postings: postings
463 .into_iter()
464 .map(
465 |(entity_id, term_count)| reddb_file::NativeFulltextPosting {
466 entity_id,
467 term_count,
468 },
469 )
470 .collect(),
471 })
472 .collect();
473 reddb_file::encode_native_fulltext_frame(&reddb_file::NativeFulltextFrame {
474 collection: collection.to_string(),
475 doc_count: documents.len() as u32,
476 terms,
477 })
478 }
479
480 pub(crate) fn inspect_native_fulltext_artifact(
481 bytes: &[u8],
482 ) -> Result<(u64, u64, u64), String> {
483 let frame = reddb_file::decode_native_fulltext_frame(bytes).map_err(|e| e.to_string())?;
484 let posting_count: u64 = frame
485 .terms
486 .iter()
487 .map(|term| term.postings.len() as u64)
488 .sum();
489 Ok((
490 frame.doc_count as u64,
491 frame.terms.len() as u64,
492 posting_count,
493 ))
494 }
495
496 fn serialize_native_document_pathvalue_artifact(
497 collection: &str,
498 documents: &[(EntityId, Vec<(String, String)>)],
499 ) -> Vec<u8> {
500 let docs: Vec<reddb_file::NativeDocPathValue> = documents
501 .iter()
502 .map(|(entity_id, entries)| reddb_file::NativeDocPathValue {
503 entity_id: entity_id.raw(),
504 entries: entries
505 .iter()
506 .map(|(path, value)| reddb_file::NativeDocPathValueEntry {
507 path: path.clone(),
508 value: value.clone(),
509 })
510 .collect(),
511 })
512 .collect();
513 reddb_file::encode_native_doc_pathvalue_frame(&reddb_file::NativeDocPathValueFrame {
514 collection: collection.to_string(),
515 docs,
516 })
517 }
518
519 pub(crate) fn inspect_native_document_pathvalue_artifact(
520 bytes: &[u8],
521 ) -> Result<(u64, u64, u64, u64), String> {
522 let frame =
523 reddb_file::decode_native_doc_pathvalue_frame(bytes).map_err(|e| e.to_string())?;
524 let doc_count = frame.docs.len() as u64;
525 let mut paths = BTreeSet::new();
526 let mut values = BTreeSet::new();
527 let mut total_entries = 0u64;
528 for doc in frame.docs {
529 for entry in doc.entries {
530 paths.insert(entry.path);
531 values.insert(entry.value);
532 total_entries += 1;
533 }
534 }
535 Ok((
536 doc_count,
537 paths.len() as u64,
538 total_entries,
539 values.len() as u64,
540 ))
541 }
542
543 fn native_document_pathvalue_for_entity(
544 entity_id: EntityId,
545 data: &EntityData,
546 limit: usize,
547 ) -> Result<Option<(EntityId, Vec<(String, String)>)>, ()> {
548 let mut entries = Vec::new();
549 let mut bytes = 0;
550 match data {
551 EntityData::Row(row) => {
552 if row.named.is_some() || row.schema.is_some() {
553 for (key, value) in row.iter_fields() {
554 Self::collect_native_document_entries_from_value(
555 key,
556 value,
557 &mut entries,
558 &mut bytes,
559 limit,
560 )?;
561 }
562 } else {
563 for (idx, value) in row.columns.iter().enumerate() {
564 let path = format!("columns[{idx}]");
565 Self::collect_native_document_entries_from_value(
566 &path,
567 value,
568 &mut entries,
569 &mut bytes,
570 limit,
571 )?;
572 }
573 }
574 }
575 EntityData::Node(node) => {
576 for (key, value) in &node.properties {
577 Self::collect_native_document_entries_from_value(
578 key,
579 value,
580 &mut entries,
581 &mut bytes,
582 limit,
583 )?;
584 }
585 }
586 EntityData::Edge(edge) => {
587 for (key, value) in &edge.properties {
588 Self::collect_native_document_entries_from_value(
589 key,
590 value,
591 &mut entries,
592 &mut bytes,
593 limit,
594 )?;
595 }
596 }
597 EntityData::Vector(_) => {}
598 EntityData::TimeSeries(_) => {}
599 EntityData::QueueMessage(_) => {}
600 }
601 if entries.is_empty() {
602 Ok(None)
603 } else {
604 Ok(Some((entity_id, entries)))
605 }
606 }
607
608 fn collect_native_document_entries_from_value(
609 path: &str,
610 value: &Value,
611 out: &mut Vec<(String, String)>,
612 bytes: &mut usize,
613 limit: usize,
614 ) -> Result<(), ()> {
615 match value {
616 Value::Json(payload) | Value::Blob(payload) => {
617 if let Ok(json) = crate::json::from_slice::<JsonValue>(payload) {
618 Self::collect_native_document_entries_from_json(
619 path, &json, out, bytes, limit,
620 )?;
621 }
622 }
623 _ => {}
624 }
625 Ok(())
626 }
627
628 fn collect_native_document_entries_from_json(
629 path: &str,
630 value: &JsonValue,
631 out: &mut Vec<(String, String)>,
632 bytes: &mut usize,
633 limit: usize,
634 ) -> Result<(), ()> {
635 let mut pending = vec![(path.to_string(), value)];
636 let mut pending_bytes = path.len() + std::mem::size_of::<(String, &JsonValue)>();
637 while let Some((path, value)) = pending.pop() {
638 pending_bytes -= path.len() + std::mem::size_of::<(String, &JsonValue)>();
639 match value {
640 JsonValue::Object(entries) => {
641 for (key, value) in entries.iter().rev() {
642 let next = if path.is_empty() {
643 key.clone()
644 } else {
645 format!("{path}.{key}")
646 };
647 let cost = next.len() + std::mem::size_of::<(String, &JsonValue)>();
648 if cost > limit.saturating_sub(bytes.saturating_add(pending_bytes)) {
649 return Err(());
650 }
651 pending_bytes += cost;
652 pending.push((next, value));
653 }
654 }
655 JsonValue::Array(items) => {
656 for (idx, value) in items.iter().enumerate().rev() {
657 let next = format!("{path}[{idx}]");
658 let cost = next.len() + std::mem::size_of::<(String, &JsonValue)>();
659 if cost > limit.saturating_sub(bytes.saturating_add(pending_bytes)) {
660 return Err(());
661 }
662 pending_bytes += cost;
663 pending.push((next, value));
664 }
665 }
666 _ => {
667 if let Some(text) = Self::native_json_scalar_text(value) {
668 let cost =
669 path.len() + text.len() + std::mem::size_of::<(String, String)>();
670 if cost > limit.saturating_sub(bytes.saturating_add(pending_bytes)) {
671 return Err(());
672 }
673 *bytes += cost;
674 out.push((path, text));
675 }
676 }
677 }
678 }
679 Ok(())
680 }
681
682 fn native_json_scalar_text(value: &JsonValue) -> Option<String> {
683 match value {
684 JsonValue::Null => None,
685 JsonValue::Bool(value) => Some(value.to_string()),
686 JsonValue::Integer(value) => Some(value.to_string()),
687 JsonValue::Number(value) => Some(value.to_string()),
688 JsonValue::Decimal(value) => Some(value.clone()),
689 JsonValue::String(value) => Some(value.clone()),
690 JsonValue::Array(_) | JsonValue::Object(_) => None,
691 }
692 }
693
694 fn native_fulltext_text_for_entity(data: &EntityData) -> String {
695 match data {
696 EntityData::Row(row) => {
697 let mut parts = Vec::new();
698 if let Some(named) = &row.named {
699 for value in named.values() {
700 if let Some(text) = Self::native_value_text(value) {
701 parts.push(text);
702 }
703 }
704 }
705 for value in &row.columns {
706 if let Some(text) = Self::native_value_text(value) {
707 parts.push(text);
708 }
709 }
710 parts.join(" ")
711 }
712 EntityData::Node(node) => node
713 .properties
714 .values()
715 .filter_map(Self::native_value_text)
716 .collect::<Vec<_>>()
717 .join(" "),
718 EntityData::Edge(edge) => edge
719 .properties
720 .values()
721 .filter_map(Self::native_value_text)
722 .collect::<Vec<_>>()
723 .join(" "),
724 EntityData::Vector(vector) => vector.content.clone().unwrap_or_default(),
725 EntityData::TimeSeries(ts) => ts.metric.clone(),
726 EntityData::QueueMessage(_) => String::new(),
727 }
728 }
729
730 fn native_value_text(value: &Value) -> Option<String> {
731 match value {
732 Value::Text(value) => Some(value.to_string()),
733 Value::Json(value) => String::from_utf8(value.clone()).ok(),
734 Value::Blob(value) => String::from_utf8(value.clone()).ok(),
735 Value::Integer(value) => Some(value.to_string()),
736 Value::UnsignedInteger(value) => Some(value.to_string()),
737 Value::Float(value) => Some(value.to_string()),
738 Value::Boolean(value) => Some(value.to_string()),
739 Value::IpAddr(value) => Some(value.to_string()),
740 Value::NodeRef(value) => Some(value.clone()),
741 Value::EdgeRef(value) => Some(value.clone()),
742 Value::RowRef(table, row_id) => Some(format!("{table}:{row_id}")),
743 Value::VectorRef(collection, vector_id) => Some(format!("{collection}:{vector_id}")),
744 Value::Timestamp(value) => Some(value.to_string()),
745 Value::Duration(value) => Some(value.to_string()),
746 Value::Uuid(_) | Value::MacAddr(_) | Value::Vector(_) | Value::Null => None,
747 Value::Color([r, g, b]) => Some(format!("#{:02X}{:02X}{:02X}", r, g, b)),
748 Value::Email(s) => Some(s.clone()),
749 Value::Url(s) => Some(s.clone()),
750 Value::Phone(n) => Some(format!("+{}", n)),
751 Value::Semver(packed) => Some(format!(
752 "{}.{}.{}",
753 packed / 1_000_000,
754 (packed / 1_000) % 1_000,
755 packed % 1_000
756 )),
757 Value::Cidr(ip, prefix) => Some(format!(
758 "{}.{}.{}.{}/{}",
759 (ip >> 24) & 0xFF,
760 (ip >> 16) & 0xFF,
761 (ip >> 8) & 0xFF,
762 ip & 0xFF,
763 prefix
764 )),
765 Value::Date(days) => Some(days.to_string()),
766 Value::Time(ms) => {
767 let total_secs = ms / 1000;
768 Some(format!(
769 "{:02}:{:02}:{:02}",
770 total_secs / 3600,
771 (total_secs / 60) % 60,
772 total_secs % 60
773 ))
774 }
775 Value::Decimal(v) => Some(Value::Decimal(*v).display_string()),
776 Value::DecimalText(v) => Some(v.clone()),
777 Value::EnumValue(i) => Some(format!("enum({})", i)),
778 Value::Array(_) => None,
779 Value::TimestampMs(ms) => Some(ms.to_string()),
780 Value::Ipv4(ip) => Some(format!(
781 "{}.{}.{}.{}",
782 (ip >> 24) & 0xFF,
783 (ip >> 16) & 0xFF,
784 (ip >> 8) & 0xFF,
785 ip & 0xFF
786 )),
787 Value::Ipv6(bytes) => Some(format!("{}", std::net::Ipv6Addr::from(*bytes))),
788 Value::Subnet(ip, mask) => {
789 let prefix = mask.leading_ones();
790 Some(format!(
791 "{}.{}.{}.{}/{}",
792 (ip >> 24) & 0xFF,
793 (ip >> 16) & 0xFF,
794 (ip >> 8) & 0xFF,
795 ip & 0xFF,
796 prefix
797 ))
798 }
799 Value::Port(p) => Some(p.to_string()),
800 Value::Latitude(micro) => Some(format!("{:.6}", *micro as f64 / 1_000_000.0)),
801 Value::Longitude(micro) => Some(format!("{:.6}", *micro as f64 / 1_000_000.0)),
802 Value::GeoPoint(lat, lon) => Some(format!(
803 "{:.6},{:.6}",
804 *lat as f64 / 1_000_000.0,
805 *lon as f64 / 1_000_000.0
806 )),
807 Value::Country2(c) => Some(String::from_utf8_lossy(c).to_string()),
808 Value::Country3(c) => Some(String::from_utf8_lossy(c).to_string()),
809 Value::Lang2(c) => Some(String::from_utf8_lossy(c).to_string()),
810 Value::Lang5(c) => Some(String::from_utf8_lossy(c).to_string()),
811 Value::Currency(c) => Some(String::from_utf8_lossy(c).to_string()),
812 Value::AssetCode(code) => Some(code.clone()),
813 Value::Money { .. } => Some(value.display_string()),
814 Value::ColorAlpha([r, g, b, a]) => {
815 Some(format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a))
816 }
817 Value::BigInt(v) => Some(v.to_string()),
818 Value::KeyRef(col, key) => Some(format!("{}:{}", col, key)),
819 Value::DocRef(col, id) => Some(format!("{}#{}", col, id)),
820 Value::TableRef(name) => Some(name.clone()),
821 Value::PageRef(page_id) => Some(format!("page:{}", page_id)),
822 Value::Secret(_) | Value::Password(_) => None,
823 }
824 }
825
826 fn native_fulltext_tokenize(text: &str) -> Vec<String> {
827 text.to_lowercase()
828 .split(|c: char| !c.is_alphanumeric())
829 .filter(|s| s.len() >= 2)
830 .map(|s| s.to_string())
831 .collect()
832 }
833
834 pub(crate) fn native_recovery_summary_from_metadata(
835 metadata: &PhysicalMetadataFile,
836 ) -> NativeRecoverySummary {
837 const SAMPLE_LIMIT: usize = 16;
838
839 let snapshots: Vec<_> = metadata
840 .snapshots
841 .iter()
842 .rev()
843 .take(SAMPLE_LIMIT)
844 .map(|snapshot| NativeSnapshotSummary {
845 snapshot_id: snapshot.snapshot_id,
846 created_at_unix_ms: snapshot.created_at_unix_ms,
847 superblock_sequence: snapshot.superblock_sequence,
848 collection_count: snapshot.collection_count as u32,
849 total_entities: snapshot.total_entities as u64,
850 })
851 .collect();
852 let exports: Vec<_> = metadata
853 .exports
854 .iter()
855 .rev()
856 .take(SAMPLE_LIMIT)
857 .map(|export| NativeExportSummary {
858 name: export.name.clone(),
859 created_at_unix_ms: export.created_at_unix_ms,
860 snapshot_id: export.snapshot_id,
861 superblock_sequence: export.superblock_sequence,
862 collection_count: export.collection_count as u32,
863 total_entities: export.total_entities as u64,
864 })
865 .collect();
866
867 NativeRecoverySummary {
868 snapshot_count: metadata.snapshots.len() as u32,
869 export_count: metadata.exports.len() as u32,
870 snapshots_complete: metadata.snapshots.len() <= SAMPLE_LIMIT,
871 exports_complete: metadata.exports.len() <= SAMPLE_LIMIT,
872 omitted_snapshot_count: metadata.snapshots.len().saturating_sub(snapshots.len()) as u32,
873 omitted_export_count: metadata.exports.len().saturating_sub(exports.len()) as u32,
874 snapshots,
875 exports,
876 }
877 }
878
879 pub(crate) fn native_catalog_summary_from_metadata(
880 metadata: &PhysicalMetadataFile,
881 ) -> NativeCatalogSummary {
882 const SAMPLE_LIMIT: usize = 32;
883
884 let collections: Vec<_> = metadata
885 .catalog
886 .stats_by_collection
887 .iter()
888 .take(SAMPLE_LIMIT)
889 .map(|(name, stats)| NativeCatalogCollectionSummary {
890 name: name.clone(),
891 entities: stats.entities as u64,
892 cross_refs: stats.cross_refs as u64,
893 segments: stats.segments as u32,
894 })
895 .collect();
896
897 NativeCatalogSummary {
898 collection_count: metadata.catalog.total_collections as u32,
899 total_entities: metadata.catalog.total_entities as u64,
900 collections_complete: metadata.catalog.stats_by_collection.len() <= SAMPLE_LIMIT,
901 omitted_collection_count: metadata
902 .catalog
903 .stats_by_collection
904 .len()
905 .saturating_sub(collections.len()) as u32,
906 collections,
907 }
908 }
909
910 pub(crate) fn native_metadata_state_summary_from_metadata(
911 metadata: &PhysicalMetadataFile,
912 ) -> NativeMetadataStateSummary {
913 NativeMetadataStateSummary {
914 protocol_version: metadata.protocol_version.clone(),
915 generated_at_unix_ms: metadata.generated_at_unix_ms,
916 last_loaded_from: metadata.last_loaded_from.clone(),
917 last_healed_at_unix_ms: metadata.last_healed_at_unix_ms,
918 }
919 }
920
921 pub(crate) fn inspect_native_header_against_metadata(
922 native: PhysicalFileHeader,
923 metadata: &PhysicalMetadataFile,
924 ) -> NativeHeaderInspection {
925 let expected = Self::native_header_from_metadata(metadata);
926 let mut mismatches = Vec::new();
927
928 if native.format_version != expected.format_version {
929 mismatches.push(NativeHeaderMismatch {
930 field: "format_version",
931 native: native.format_version.to_string(),
932 expected: expected.format_version.to_string(),
933 });
934 }
935 if native.sequence != expected.sequence {
936 mismatches.push(NativeHeaderMismatch {
937 field: "sequence",
938 native: native.sequence.to_string(),
939 expected: expected.sequence.to_string(),
940 });
941 }
942 if native.manifest_oldest_root != expected.manifest_oldest_root {
943 mismatches.push(NativeHeaderMismatch {
944 field: "manifest_oldest_root",
945 native: native.manifest_oldest_root.to_string(),
946 expected: expected.manifest_oldest_root.to_string(),
947 });
948 }
949 if native.manifest_root != expected.manifest_root {
950 mismatches.push(NativeHeaderMismatch {
951 field: "manifest_root",
952 native: native.manifest_root.to_string(),
953 expected: expected.manifest_root.to_string(),
954 });
955 }
956 if native.free_set_root != expected.free_set_root {
957 mismatches.push(NativeHeaderMismatch {
958 field: "free_set_root",
959 native: native.free_set_root.to_string(),
960 expected: expected.free_set_root.to_string(),
961 });
962 }
963 if native.collection_root_count != expected.collection_root_count {
964 mismatches.push(NativeHeaderMismatch {
965 field: "collection_root_count",
966 native: native.collection_root_count.to_string(),
967 expected: expected.collection_root_count.to_string(),
968 });
969 }
970 if native.snapshot_count != expected.snapshot_count {
971 mismatches.push(NativeHeaderMismatch {
972 field: "snapshot_count",
973 native: native.snapshot_count.to_string(),
974 expected: expected.snapshot_count.to_string(),
975 });
976 }
977 if native.index_count != expected.index_count {
978 mismatches.push(NativeHeaderMismatch {
979 field: "index_count",
980 native: native.index_count.to_string(),
981 expected: expected.index_count.to_string(),
982 });
983 }
984 if native.catalog_collection_count != expected.catalog_collection_count {
985 mismatches.push(NativeHeaderMismatch {
986 field: "catalog_collection_count",
987 native: native.catalog_collection_count.to_string(),
988 expected: expected.catalog_collection_count.to_string(),
989 });
990 }
991 if native.catalog_total_entities != expected.catalog_total_entities {
992 mismatches.push(NativeHeaderMismatch {
993 field: "catalog_total_entities",
994 native: native.catalog_total_entities.to_string(),
995 expected: expected.catalog_total_entities.to_string(),
996 });
997 }
998 if native.export_count != expected.export_count {
999 mismatches.push(NativeHeaderMismatch {
1000 field: "export_count",
1001 native: native.export_count.to_string(),
1002 expected: expected.export_count.to_string(),
1003 });
1004 }
1005 if native.graph_projection_count != expected.graph_projection_count {
1006 mismatches.push(NativeHeaderMismatch {
1007 field: "graph_projection_count",
1008 native: native.graph_projection_count.to_string(),
1009 expected: expected.graph_projection_count.to_string(),
1010 });
1011 }
1012 if native.analytics_job_count != expected.analytics_job_count {
1013 mismatches.push(NativeHeaderMismatch {
1014 field: "analytics_job_count",
1015 native: native.analytics_job_count.to_string(),
1016 expected: expected.analytics_job_count.to_string(),
1017 });
1018 }
1019 if native.manifest_event_count != expected.manifest_event_count {
1020 mismatches.push(NativeHeaderMismatch {
1021 field: "manifest_event_count",
1022 native: native.manifest_event_count.to_string(),
1023 expected: expected.manifest_event_count.to_string(),
1024 });
1025 }
1026
1027 NativeHeaderInspection {
1028 native,
1029 expected,
1030 consistent: mismatches.is_empty(),
1031 mismatches,
1032 }
1033 }
1034
1035 pub(crate) fn repair_policy_for_inspection(
1036 inspection: &NativeHeaderInspection,
1037 ) -> NativeHeaderRepairPolicy {
1038 if inspection.consistent {
1039 return NativeHeaderRepairPolicy::InSync;
1040 }
1041
1042 if inspection.expected.sequence >= inspection.native.sequence {
1043 NativeHeaderRepairPolicy::RepairNativeFromMetadata
1044 } else {
1045 NativeHeaderRepairPolicy::NativeAheadOfMetadata
1046 }
1047 }
1048
1049 pub(crate) fn prune_export_registry(&self, exports: &mut Vec<ExportDescriptor>) {
1050 let retention = self.options.export_retention.max(1);
1051 if exports.len() <= retention {
1052 return;
1053 }
1054
1055 exports.sort_by_key(|export| export.created_at_unix_ms);
1056 let removed: Vec<ExportDescriptor> =
1057 exports.drain(0..(exports.len() - retention)).collect();
1058
1059 for export in removed {
1060 let _ = fs::remove_file(&export.data_path);
1061 let _ = fs::remove_file(&export.metadata_path);
1062 let binary_path = PhysicalMetadataFile::metadata_binary_path_for(std::path::Path::new(
1063 &export.data_path,
1064 ));
1065 let _ = fs::remove_file(binary_path);
1066 }
1067 }
1068
1069 pub(crate) fn runtime_index_catalog(&self) -> IndexCatalog {
1070 let mut catalog = IndexCatalog::register_default_vector_graph(
1071 self.options.has_capability(Capability::Table),
1072 self.options.has_capability(Capability::Graph),
1073 );
1074 if self.options.has_capability(Capability::FullText) {
1075 catalog.register(RuntimeIndexConfig::new(
1076 "text-fulltext",
1077 IndexKind::FullText,
1078 ));
1079 catalog.register(RuntimeIndexConfig::new(
1080 "document-pathvalue",
1081 IndexKind::DocumentPathValue,
1082 ));
1083 }
1084 catalog.register(RuntimeIndexConfig::new(
1085 "search-hybrid",
1086 IndexKind::HybridSearch,
1087 ));
1088 catalog
1089 }
1090
1091 pub(crate) fn physical_index_state(&self) -> Vec<PhysicalIndexState> {
1092 let catalog = self.runtime_index_catalog();
1096 let snapshot = crate::catalog::snapshot_store_with_declarations(
1097 "reddb",
1098 self.store.as_ref(),
1099 Some(&catalog),
1100 None, None, );
1103 let mut metrics_by_name = std::collections::BTreeMap::new();
1104 for metric in &snapshot.indices {
1105 metrics_by_name.insert(metric.name.clone(), metric.clone());
1106 }
1107
1108 let mut states = Vec::new();
1109 for collection in snapshot.collections {
1110 for index_name in &collection.indices {
1111 let metric = metrics_by_name.get(index_name);
1112 let kind = metric
1113 .map(|metric| metric.kind)
1114 .unwrap_or_else(|| infer_collection_index_kind(collection.model, index_name));
1115 let entries = estimate_index_entries(&collection, kind);
1116 states.push(PhysicalIndexState {
1117 name: format!("{}::{}", collection.name, index_name),
1118 kind,
1119 collection: Some(collection.name.clone()),
1120 enabled: metric.map(|metric| metric.enabled).unwrap_or(true),
1121 entries,
1122 estimated_memory_bytes: estimate_index_memory(entries, kind),
1123 last_refresh_ms: metric.and_then(|metric| metric.last_refresh_ms),
1124 backend: index_backend_name(kind).to_string(),
1125 artifact_kind: None,
1126 artifact_root_page: None,
1127 artifact_checksum: None,
1128 build_state: "catalog-derived".to_string(),
1129 });
1130 }
1131 }
1132
1133 states
1134 }
1135
1136 pub(crate) fn physical_collection_roots(&self) -> BTreeMap<String, u64> {
1137 let mut roots = BTreeMap::new();
1138
1139 for name in self.store.list_collections() {
1140 let Some(manager) = self.store.get_collection(&name) else {
1141 continue;
1142 };
1143
1144 let stats = manager.stats();
1145 let mut root = fnv1a_seed();
1146 fnv1a_hash_value(&mut root, &name);
1147 fnv1a_hash_value(&mut root, &stats.total_entities);
1148 fnv1a_hash_value(&mut root, &stats.growing_count);
1149 fnv1a_hash_value(&mut root, &stats.sealed_count);
1150 fnv1a_hash_value(&mut root, &stats.archived_count);
1151 fnv1a_hash_value(&mut root, &stats.total_memory_bytes);
1152 fnv1a_hash_value(&mut root, &stats.seal_ops);
1153 fnv1a_hash_value(&mut root, &stats.consolidation.runs_completed);
1154
1155 let root = manager.cached_physical_root(root, || {
1156 manager.for_each_entity_in_id_order(|entity| {
1157 fnv1a_hash_value(&mut root, &entity.id.raw());
1158 fnv1a_hash_value(&mut root, &entity.kind);
1159 fnv1a_hash_value(&mut root, &entity.created_at);
1160 fnv1a_hash_value(&mut root, &entity.updated_at);
1161 fnv1a_hash_value(&mut root, &entity.data);
1162 fnv1a_hash_value(&mut root, &entity.sequence_id);
1163 fnv1a_hash_value(&mut root, &entity.embeddings().len());
1164 fnv1a_hash_value(&mut root, &entity.cross_refs().len());
1165 true
1166 });
1167 root
1168 });
1169
1170 roots.insert(name, root);
1171 }
1172
1173 roots
1174 }
1175
1176 pub fn table_ref(&self, table: impl Into<String>, row_id: u64) -> TableRef {
1182 TableRef::new(table, row_id)
1183 }
1184
1185 pub fn node_ref(&self, collection: impl Into<String>, node_id: EntityId) -> NodeRef {
1187 NodeRef::new(collection, node_id)
1188 }
1189
1190 pub fn vector_ref(&self, collection: impl Into<String>, vector_id: EntityId) -> VectorRef {
1192 VectorRef::new(collection, vector_id)
1193 }
1194
1195 pub fn query(&self) -> QueryBuilder {
1201 QueryBuilder::new(self.store.clone())
1202 }
1203
1204 pub fn similar(&self, collection: &str, vector: &[f32], k: usize) -> Vec<SimilarResult> {
1211 if self.store.get_collection(collection).is_none() {
1212 return Vec::new();
1213 }
1214
1215 if let Some(index) = self.get_or_build_hnsw_index(collection, vector.len()) {
1217 let hnsw = index.read().unwrap_or_else(|e| e.into_inner());
1218 let results = hnsw.search(vector, k);
1219 let mapped = self.hnsw_results_to_similar(collection, &results);
1220 if !mapped.is_empty() {
1221 return mapped;
1222 }
1223 }
1224
1225 self.similar_brute_force(collection, vector, k)
1227 }
1228
1229 fn similar_brute_force(
1231 &self,
1232 collection: &str,
1233 vector: &[f32],
1234 k: usize,
1235 ) -> Vec<SimilarResult> {
1236 let manager = match self.store.get_collection(collection) {
1237 Some(m) => m,
1238 None => return Vec::new(),
1239 };
1240
1241 let entities = manager.query_all(|_| true);
1242 let mut results: Vec<SimilarResult> = entities
1243 .iter()
1244 .filter_map(|e| {
1245 let score = match &e.data {
1246 EntityData::Vector(v) => cosine_similarity(vector, &v.dense),
1247 _ => e
1248 .embeddings()
1249 .iter()
1250 .map(|emb| cosine_similarity(vector, &emb.vector))
1251 .fold(0.0f32, f32::max),
1252 };
1253 let distance = (1.0 - score).max(0.0);
1254 if score > 0.0 {
1255 Some(SimilarResult {
1256 entity_id: e.id,
1257 score,
1258 distance,
1259 entity: e.clone(),
1260 })
1261 } else {
1262 None
1263 }
1264 })
1265 .collect();
1266
1267 results.sort_by(|a, b| {
1268 b.score
1269 .partial_cmp(&a.score)
1270 .unwrap_or(std::cmp::Ordering::Equal)
1271 });
1272 results.truncate(k);
1273 results
1274 }
1275
1276 fn get_or_build_hnsw_index(
1286 &self,
1287 collection: &str,
1288 query_dim: usize,
1289 ) -> Option<Arc<RwLock<HnswIndex>>> {
1290 let manager = self.store.get_collection(collection)?;
1291 let live_count = manager.count();
1292
1293 {
1295 let indexes = self
1296 .vector_indexes
1297 .read()
1298 .unwrap_or_else(|e| e.into_inner());
1299 if let Some(cached) = indexes.get(collection) {
1300 if cached.entity_count == live_count {
1301 return Some(Arc::clone(&cached.index));
1302 }
1303 }
1304 }
1305
1306 let entities = manager.query_all(|_| true);
1308
1309 let vectors: Vec<(u64, Vec<f32>)> = entities
1310 .iter()
1311 .filter_map(|e| match &e.data {
1312 EntityData::Vector(v) if !v.dense.is_empty() && v.dense.len() == query_dim => {
1313 Some((e.id.raw(), v.dense.clone()))
1314 }
1315 _ => None,
1316 })
1317 .collect();
1318
1319 const MIN_VECTORS_FOR_HNSW: usize = 100;
1321 if vectors.len() < MIN_VECTORS_FOR_HNSW {
1322 return None;
1323 }
1324
1325 let config = crate::storage::engine::HnswConfig::with_m(16)
1327 .with_metric(crate::storage::engine::DistanceMetric::Cosine)
1328 .with_ef_construction(100)
1329 .with_ef_search(50);
1330 let mut hnsw = HnswIndex::new(query_dim, config);
1331
1332 for (id, vec) in &vectors {
1333 hnsw.insert_with_id(*id, vec.clone());
1334 }
1335
1336 let index = Arc::new(RwLock::new(hnsw));
1337
1338 let mut indexes = self
1340 .vector_indexes
1341 .write()
1342 .unwrap_or_else(|e| e.into_inner());
1343 if let Some(cached) = indexes.get(collection) {
1345 if cached.entity_count == live_count {
1346 return Some(Arc::clone(&cached.index));
1347 }
1348 }
1349 indexes.insert(
1350 collection.to_string(),
1351 CachedVectorIndex {
1352 index: Arc::clone(&index),
1353 entity_count: live_count,
1354 },
1355 );
1356 Some(index)
1357 }
1358
1359 fn hnsw_results_to_similar(
1361 &self,
1362 collection: &str,
1363 results: &[crate::storage::engine::DistanceResult],
1364 ) -> Vec<SimilarResult> {
1365 results
1366 .iter()
1367 .filter_map(|dr| {
1368 let entity_id = EntityId::new(dr.id);
1369 let entity = self.store.get(collection, entity_id)?;
1370 let score = (1.0 - dr.distance).max(0.0);
1372 if score > 0.0 {
1373 Some(SimilarResult {
1374 entity_id,
1375 score,
1376 distance: dr.distance,
1377 entity,
1378 })
1379 } else {
1380 None
1381 }
1382 })
1383 .collect()
1384 }
1385
1386 pub(crate) fn invalidate_vector_index(&self, collection: &str) {
1391 let mut indexes = self
1392 .vector_indexes
1393 .write()
1394 .unwrap_or_else(|e| e.into_inner());
1395 indexes.remove(collection);
1396 }
1397
1398 pub fn get(&self, id: EntityId) -> Option<UnifiedEntity> {
1400 self.store.get_any(id).map(|(_, e)| e)
1401 }
1402
1403 pub fn get_with_collection(&self, id: EntityId) -> Option<(String, UnifiedEntity)> {
1405 self.store.get_any(id)
1406 }
1407
1408 pub fn batch_get(&self, ids: &[EntityId]) -> Vec<Option<UnifiedEntity>> {
1414 ids.iter().map(|id| self.get(*id)).collect()
1415 }
1416
1417 pub fn batch(&self) -> BatchBuilder {
1419 BatchBuilder::new(self.store.clone(), self.preprocessors.clone())
1420 }
1421
1422 pub fn add_preprocessor(&mut self, preprocessor: Box<dyn Preprocessor>) {
1428 let mut preprocessors = self
1429 .preprocessors
1430 .write()
1431 .unwrap_or_else(|poisoned| poisoned.into_inner());
1432 preprocessors.push(Arc::from(preprocessor));
1433 }
1434
1435 pub fn linked_from(&self, id: EntityId) -> Vec<LinkedEntity> {
1441 self.store
1442 .get_refs_from(id)
1443 .into_iter()
1444 .filter_map(|(target_id, ref_type, collection)| {
1445 self.store
1446 .get(&collection, target_id)
1447 .map(|entity| LinkedEntity {
1448 entity,
1449 ref_type,
1450 collection,
1451 })
1452 })
1453 .collect()
1454 }
1455
1456 pub fn linked_to(&self, id: EntityId) -> Vec<LinkedEntity> {
1458 self.store
1459 .get_refs_to(id)
1460 .into_iter()
1461 .filter_map(|(source_id, ref_type, collection)| {
1462 self.store
1463 .get(&collection, source_id)
1464 .map(|entity| LinkedEntity {
1465 entity,
1466 ref_type,
1467 collection,
1468 })
1469 })
1470 .collect()
1471 }
1472
1473 pub fn store(&self) -> Arc<UnifiedStore> {
1475 self.store.clone()
1476 }
1477
1478 pub(crate) fn turbo_collections(
1484 &self,
1485 ) -> &Arc<
1486 parking_lot::Mutex<
1487 std::collections::HashMap<
1488 String,
1489 Arc<crate::runtime::vector_turbo_kind::TurboCollectionState>,
1490 >,
1491 >,
1492 > {
1493 self.turbo_collections
1494 .get_or_init(|| Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new())))
1495 }
1496
1497 pub(crate) fn turbo_state(
1507 &self,
1508 collection: &str,
1509 ) -> Option<Arc<crate::runtime::vector_turbo_kind::TurboCollectionState>> {
1510 if !crate::runtime::vector_turbo_kind::is_turbo(&self.store, collection) {
1511 return None;
1512 }
1513 let map = self.turbo_collections();
1514 {
1515 let guard = map.lock();
1516 if let Some(state) = guard.get(collection) {
1517 return Some(Arc::clone(state));
1518 }
1519 }
1520 let contract = self.collection_contract(collection)?;
1521 let dim = contract.vector_dimension?;
1522 let metric = contract
1523 .vector_metric
1524 .unwrap_or(crate::storage::engine::distance::DistanceMetric::Cosine);
1525 let state = Arc::new(
1526 crate::runtime::vector_turbo_kind::TurboCollectionState::new(
1527 dim,
1528 metric,
1529 self.store.pager(),
1530 ),
1531 );
1532 if let Some((_, paths)) = self.options.resolve_tiered_layout() {
1537 state.set_snapshot_path(paths.turbo_snapshot_path(collection));
1538 }
1539 let mut guard = map.lock();
1540 let inserted_now = !guard.contains_key(collection);
1541 let entry = guard
1542 .entry(collection.to_string())
1543 .or_insert_with(|| Arc::clone(&state));
1544 let handle = Arc::clone(entry);
1545 drop(guard);
1546 if inserted_now {
1553 let join = crate::runtime::vector_turbo_kind::spawn_background_rebuild(
1554 Arc::clone(&self.store),
1555 collection.to_string(),
1556 Arc::clone(&handle),
1557 );
1558 self.turbo_rebuild_workers.lock().push(join);
1559 }
1560 Some(handle)
1561 }
1562
1563 pub fn ml_runtime(&self) -> &crate::storage::ml::MlRuntime {
1569 self.ml_runtime.get_or_init(|| {
1570 crate::storage::ml::MlRuntime::in_memory(std::sync::Arc::new(
1571 |_handle| Ok(String::new()),
1575 ))
1576 })
1577 }
1578
1579 pub fn semantic_cache(&self) -> &Arc<crate::storage::ml::SemanticCache> {
1583 self.semantic_cache.get_or_init(|| {
1584 Arc::new(crate::storage::ml::SemanticCache::new(
1585 crate::storage::ml::SemanticCacheConfig::default(),
1586 ))
1587 })
1588 }
1589
1590 pub fn hypertables(&self) -> &Arc<crate::storage::timeseries::HypertableRegistry> {
1593 self.hypertables
1594 .get_or_init(|| Arc::new(crate::storage::timeseries::HypertableRegistry::new()))
1595 }
1596
1597 pub fn continuous_aggregates(
1600 &self,
1601 ) -> &Arc<crate::storage::timeseries::continuous_aggregate::ContinuousAggregateEngine> {
1602 self.continuous_aggregates.get_or_init(|| {
1603 Arc::new(
1604 crate::storage::timeseries::continuous_aggregate::ContinuousAggregateEngine::new(),
1605 )
1606 })
1607 }
1608
1609 pub(crate) fn is_binary_dump(path: &Path) -> Result<bool, std::io::Error> {
1610 let mut file = File::open(path)?;
1611 let mut magic = [0u8; 4];
1612 let read = file.read(&mut magic)?;
1613 Ok(read == 4 && reddb_file::native_store_magic_matches(&magic))
1614 }
1615}
1616
1617#[cfg(test)]
1618mod artifact_memory_tests {
1619 use super::*;
1620
1621 #[test]
1622 fn document_artifact_paths_survive_shared_recovery_shapes() {
1623 let mut entity = UnifiedEntity::table_row(EntityId::new(1), "documents", 1, vec![]);
1624 entity.data = EntityData::Row(crate::storage::unified::entity::RowData::with_names(
1625 vec![Value::Json(br#"{"thing":"kept"}"#.to_vec())],
1626 vec!["body".into()],
1627 ));
1628 let before = RedDB::native_document_pathvalue_for_entity(entity.id, &entity.data, 4096)
1629 .expect("named document");
1630 let EntityData::Row(row) = &mut entity.data else {
1631 unreachable!("fixture is a row");
1632 };
1633 *row = crate::storage::unified::entity::RowData {
1634 columns: vec![row.get_field("body").expect("body field").clone()],
1635 named: None,
1636 schema: Some(Arc::new(vec!["body".into()])),
1637 };
1638 let after = RedDB::native_document_pathvalue_for_entity(entity.id, &entity.data, 4096)
1639 .expect("shared document");
1640 assert_eq!(
1641 before, after,
1642 "compaction must preserve named document paths"
1643 );
1644 }
1645
1646 #[test]
1647 fn disabled_fulltext_does_not_copy_rows_or_publish_search_artifacts() {
1648 let runtime =
1649 crate::RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
1650 runtime
1651 .execute_query("CREATE TABLE source (payload BLOB)")
1652 .expect("table");
1653 let db = runtime.db();
1654 let manager = db.store().get_collection("source").expect("manager");
1655 let (_, baseline) = crate::storage::no_malloc_hot_paths::measure_allocations(|| {
1656 db.native_vector_artifact_records().expect("empty build")
1657 });
1658 for id in 1..=32 {
1659 manager
1660 .insert(UnifiedEntity::table_row(
1661 EntityId::new(1000 + id),
1662 "source",
1663 id,
1664 vec![Value::Blob(vec![255; 64 * 1024])],
1665 ))
1666 .expect("source row");
1667 }
1668 let (build, measured) = crate::storage::no_malloc_hot_paths::measure_allocations(|| {
1669 db.native_vector_artifact_records().expect("disabled build")
1670 });
1671 assert!(build.records.is_empty());
1672 assert!(
1673 build.deferred_collections.is_empty(),
1674 "disabled capabilities do not consume artifact allowance"
1675 );
1676 assert!(
1677 measured.allocated_bytes <= baseline.allocated_bytes + 4096,
1678 "disabled fulltext copied source payloads: {baseline:?} -> {measured:?}"
1679 );
1680 }
1681
1682 #[test]
1683 fn document_artifact_traversal_is_ordered_and_bounded() {
1684 let json = crate::json::from_slice::<JsonValue>(br#"{"a":{"b":1},"z":[2,3]}"#)
1685 .expect("JSON fixture");
1686 let mut entries = Vec::new();
1687 let mut bytes = 0;
1688 RedDB::collect_native_document_entries_from_json(
1689 "body",
1690 &json,
1691 &mut entries,
1692 &mut bytes,
1693 4096,
1694 )
1695 .expect("small document fits");
1696 assert_eq!(
1697 entries,
1698 vec![
1699 ("body.a.b".into(), "1".into()),
1700 ("body.z[0]".into(), "2".into()),
1701 ("body.z[1]".into(), "3".into())
1702 ]
1703 );
1704 let mut deep = JsonValue::Integer(1);
1705 for _ in 0..64 {
1706 deep = JsonValue::Object([("child".to_string(), deep)].into());
1707 }
1708 let mut entries = Vec::new();
1709 let mut bytes = 0;
1710 assert!(RedDB::collect_native_document_entries_from_json(
1711 "body",
1712 &deep,
1713 &mut entries,
1714 &mut bytes,
1715 128
1716 )
1717 .is_err());
1718 assert!(bytes <= 128);
1719 }
1720}