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::Temperature(_) => Some(value.display_string()),
815 Value::ColorAlpha([r, g, b, a]) => {
816 Some(format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a))
817 }
818 Value::BigInt(v) => Some(v.to_string()),
819 Value::KeyRef(col, key) => Some(format!("{}:{}", col, key)),
820 Value::DocRef(col, id) => Some(format!("{}#{}", col, id)),
821 Value::TableRef(name) => Some(name.clone()),
822 Value::PageRef(page_id) => Some(format!("page:{}", page_id)),
823 Value::Secret(_) | Value::Password(_) => None,
824 }
825 }
826
827 fn native_fulltext_tokenize(text: &str) -> Vec<String> {
828 text.to_lowercase()
829 .split(|c: char| !c.is_alphanumeric())
830 .filter(|s| s.len() >= 2)
831 .map(|s| s.to_string())
832 .collect()
833 }
834
835 pub(crate) fn native_recovery_summary_from_metadata(
836 metadata: &PhysicalMetadataFile,
837 ) -> NativeRecoverySummary {
838 const SAMPLE_LIMIT: usize = 16;
839
840 let snapshots: Vec<_> = metadata
841 .snapshots
842 .iter()
843 .rev()
844 .take(SAMPLE_LIMIT)
845 .map(|snapshot| NativeSnapshotSummary {
846 snapshot_id: snapshot.snapshot_id,
847 created_at_unix_ms: snapshot.created_at_unix_ms,
848 superblock_sequence: snapshot.superblock_sequence,
849 collection_count: snapshot.collection_count as u32,
850 total_entities: snapshot.total_entities as u64,
851 })
852 .collect();
853 let exports: Vec<_> = metadata
854 .exports
855 .iter()
856 .rev()
857 .take(SAMPLE_LIMIT)
858 .map(|export| NativeExportSummary {
859 name: export.name.clone(),
860 created_at_unix_ms: export.created_at_unix_ms,
861 snapshot_id: export.snapshot_id,
862 superblock_sequence: export.superblock_sequence,
863 collection_count: export.collection_count as u32,
864 total_entities: export.total_entities as u64,
865 })
866 .collect();
867
868 NativeRecoverySummary {
869 snapshot_count: metadata.snapshots.len() as u32,
870 export_count: metadata.exports.len() as u32,
871 snapshots_complete: metadata.snapshots.len() <= SAMPLE_LIMIT,
872 exports_complete: metadata.exports.len() <= SAMPLE_LIMIT,
873 omitted_snapshot_count: metadata.snapshots.len().saturating_sub(snapshots.len()) as u32,
874 omitted_export_count: metadata.exports.len().saturating_sub(exports.len()) as u32,
875 snapshots,
876 exports,
877 }
878 }
879
880 pub(crate) fn native_catalog_summary_from_metadata(
881 metadata: &PhysicalMetadataFile,
882 ) -> NativeCatalogSummary {
883 const SAMPLE_LIMIT: usize = 32;
884
885 let collections: Vec<_> = metadata
886 .catalog
887 .stats_by_collection
888 .iter()
889 .take(SAMPLE_LIMIT)
890 .map(|(name, stats)| NativeCatalogCollectionSummary {
891 name: name.clone(),
892 entities: stats.entities as u64,
893 cross_refs: stats.cross_refs as u64,
894 segments: stats.segments as u32,
895 })
896 .collect();
897
898 NativeCatalogSummary {
899 collection_count: metadata.catalog.total_collections as u32,
900 total_entities: metadata.catalog.total_entities as u64,
901 collections_complete: metadata.catalog.stats_by_collection.len() <= SAMPLE_LIMIT,
902 omitted_collection_count: metadata
903 .catalog
904 .stats_by_collection
905 .len()
906 .saturating_sub(collections.len()) as u32,
907 collections,
908 }
909 }
910
911 pub(crate) fn native_metadata_state_summary_from_metadata(
912 metadata: &PhysicalMetadataFile,
913 ) -> NativeMetadataStateSummary {
914 NativeMetadataStateSummary {
915 protocol_version: metadata.protocol_version.clone(),
916 generated_at_unix_ms: metadata.generated_at_unix_ms,
917 last_loaded_from: metadata.last_loaded_from.clone(),
918 last_healed_at_unix_ms: metadata.last_healed_at_unix_ms,
919 }
920 }
921
922 pub(crate) fn inspect_native_header_against_metadata(
923 native: PhysicalFileHeader,
924 metadata: &PhysicalMetadataFile,
925 ) -> NativeHeaderInspection {
926 let expected = Self::native_header_from_metadata(metadata);
927 let mut mismatches = Vec::new();
928
929 if native.format_version != expected.format_version {
930 mismatches.push(NativeHeaderMismatch {
931 field: "format_version",
932 native: native.format_version.to_string(),
933 expected: expected.format_version.to_string(),
934 });
935 }
936 if native.sequence != expected.sequence {
937 mismatches.push(NativeHeaderMismatch {
938 field: "sequence",
939 native: native.sequence.to_string(),
940 expected: expected.sequence.to_string(),
941 });
942 }
943 if native.manifest_oldest_root != expected.manifest_oldest_root {
944 mismatches.push(NativeHeaderMismatch {
945 field: "manifest_oldest_root",
946 native: native.manifest_oldest_root.to_string(),
947 expected: expected.manifest_oldest_root.to_string(),
948 });
949 }
950 if native.manifest_root != expected.manifest_root {
951 mismatches.push(NativeHeaderMismatch {
952 field: "manifest_root",
953 native: native.manifest_root.to_string(),
954 expected: expected.manifest_root.to_string(),
955 });
956 }
957 if native.free_set_root != expected.free_set_root {
958 mismatches.push(NativeHeaderMismatch {
959 field: "free_set_root",
960 native: native.free_set_root.to_string(),
961 expected: expected.free_set_root.to_string(),
962 });
963 }
964 if native.collection_root_count != expected.collection_root_count {
965 mismatches.push(NativeHeaderMismatch {
966 field: "collection_root_count",
967 native: native.collection_root_count.to_string(),
968 expected: expected.collection_root_count.to_string(),
969 });
970 }
971 if native.snapshot_count != expected.snapshot_count {
972 mismatches.push(NativeHeaderMismatch {
973 field: "snapshot_count",
974 native: native.snapshot_count.to_string(),
975 expected: expected.snapshot_count.to_string(),
976 });
977 }
978 if native.index_count != expected.index_count {
979 mismatches.push(NativeHeaderMismatch {
980 field: "index_count",
981 native: native.index_count.to_string(),
982 expected: expected.index_count.to_string(),
983 });
984 }
985 if native.catalog_collection_count != expected.catalog_collection_count {
986 mismatches.push(NativeHeaderMismatch {
987 field: "catalog_collection_count",
988 native: native.catalog_collection_count.to_string(),
989 expected: expected.catalog_collection_count.to_string(),
990 });
991 }
992 if native.catalog_total_entities != expected.catalog_total_entities {
993 mismatches.push(NativeHeaderMismatch {
994 field: "catalog_total_entities",
995 native: native.catalog_total_entities.to_string(),
996 expected: expected.catalog_total_entities.to_string(),
997 });
998 }
999 if native.export_count != expected.export_count {
1000 mismatches.push(NativeHeaderMismatch {
1001 field: "export_count",
1002 native: native.export_count.to_string(),
1003 expected: expected.export_count.to_string(),
1004 });
1005 }
1006 if native.graph_projection_count != expected.graph_projection_count {
1007 mismatches.push(NativeHeaderMismatch {
1008 field: "graph_projection_count",
1009 native: native.graph_projection_count.to_string(),
1010 expected: expected.graph_projection_count.to_string(),
1011 });
1012 }
1013 if native.analytics_job_count != expected.analytics_job_count {
1014 mismatches.push(NativeHeaderMismatch {
1015 field: "analytics_job_count",
1016 native: native.analytics_job_count.to_string(),
1017 expected: expected.analytics_job_count.to_string(),
1018 });
1019 }
1020 if native.manifest_event_count != expected.manifest_event_count {
1021 mismatches.push(NativeHeaderMismatch {
1022 field: "manifest_event_count",
1023 native: native.manifest_event_count.to_string(),
1024 expected: expected.manifest_event_count.to_string(),
1025 });
1026 }
1027
1028 NativeHeaderInspection {
1029 native,
1030 expected,
1031 consistent: mismatches.is_empty(),
1032 mismatches,
1033 }
1034 }
1035
1036 pub(crate) fn repair_policy_for_inspection(
1037 inspection: &NativeHeaderInspection,
1038 ) -> NativeHeaderRepairPolicy {
1039 if inspection.consistent {
1040 return NativeHeaderRepairPolicy::InSync;
1041 }
1042
1043 if inspection.expected.sequence >= inspection.native.sequence {
1044 NativeHeaderRepairPolicy::RepairNativeFromMetadata
1045 } else {
1046 NativeHeaderRepairPolicy::NativeAheadOfMetadata
1047 }
1048 }
1049
1050 pub(crate) fn prune_export_registry(&self, exports: &mut Vec<ExportDescriptor>) {
1051 let retention = self.options.export_retention.max(1);
1052 if exports.len() <= retention {
1053 return;
1054 }
1055
1056 exports.sort_by_key(|export| export.created_at_unix_ms);
1057 let removed: Vec<ExportDescriptor> =
1058 exports.drain(0..(exports.len() - retention)).collect();
1059
1060 for export in removed {
1061 let _ = fs::remove_file(&export.data_path);
1062 let _ = fs::remove_file(&export.metadata_path);
1063 let binary_path = PhysicalMetadataFile::metadata_binary_path_for(std::path::Path::new(
1064 &export.data_path,
1065 ));
1066 let _ = fs::remove_file(binary_path);
1067 }
1068 }
1069
1070 pub(crate) fn runtime_index_catalog(&self) -> IndexCatalog {
1071 let mut catalog = IndexCatalog::register_default_vector_graph(
1072 self.options.has_capability(Capability::Table),
1073 self.options.has_capability(Capability::Graph),
1074 );
1075 if self.options.has_capability(Capability::FullText) {
1076 catalog.register(RuntimeIndexConfig::new(
1077 "text-fulltext",
1078 IndexKind::FullText,
1079 ));
1080 catalog.register(RuntimeIndexConfig::new(
1081 "document-pathvalue",
1082 IndexKind::DocumentPathValue,
1083 ));
1084 }
1085 catalog.register(RuntimeIndexConfig::new(
1086 "search-hybrid",
1087 IndexKind::HybridSearch,
1088 ));
1089 catalog
1090 }
1091
1092 pub(crate) fn physical_index_state(&self) -> Vec<PhysicalIndexState> {
1093 let catalog = self.runtime_index_catalog();
1097 let snapshot = crate::catalog::snapshot_store_with_declarations(
1098 "reddb",
1099 self.store.as_ref(),
1100 Some(&catalog),
1101 None, None, );
1104 let mut metrics_by_name = std::collections::BTreeMap::new();
1105 for metric in &snapshot.indices {
1106 metrics_by_name.insert(metric.name.clone(), metric.clone());
1107 }
1108
1109 let mut states = Vec::new();
1110 for collection in snapshot.collections {
1111 if self
1118 .store
1119 .get_collection(&collection.name)
1120 .is_some_and(|manager| manager.has_backing())
1121 {
1122 continue;
1123 }
1124 for index_name in &collection.indices {
1125 let metric = metrics_by_name.get(index_name);
1126 let kind = metric
1127 .map(|metric| metric.kind)
1128 .unwrap_or_else(|| infer_collection_index_kind(collection.model, index_name));
1129 let entries = estimate_index_entries(&collection, kind);
1130 states.push(PhysicalIndexState {
1131 name: format!("{}::{}", collection.name, index_name),
1132 kind,
1133 collection: Some(collection.name.clone()),
1134 enabled: metric.map(|metric| metric.enabled).unwrap_or(true),
1135 entries,
1136 estimated_memory_bytes: estimate_index_memory(entries, kind),
1137 last_refresh_ms: metric.and_then(|metric| metric.last_refresh_ms),
1138 backend: index_backend_name(kind).to_string(),
1139 artifact_kind: None,
1140 artifact_root_page: None,
1141 artifact_checksum: None,
1142 build_state: "catalog-derived".to_string(),
1143 });
1144 }
1145 }
1146
1147 states
1148 }
1149
1150 pub(crate) fn physical_collection_roots(&self) -> BTreeMap<String, u64> {
1151 let mut roots = BTreeMap::new();
1152
1153 for name in self.store.list_collections() {
1154 let Some(manager) = self.store.get_collection(&name) else {
1155 continue;
1156 };
1157
1158 let stats = manager.stats();
1159 let mut root = fnv1a_seed();
1160 fnv1a_hash_value(&mut root, &name);
1161 fnv1a_hash_value(&mut root, &stats.total_entities);
1162 fnv1a_hash_value(&mut root, &stats.growing_count);
1163 fnv1a_hash_value(&mut root, &stats.sealed_count);
1164 fnv1a_hash_value(&mut root, &stats.archived_count);
1165 fnv1a_hash_value(&mut root, &stats.total_memory_bytes);
1166 fnv1a_hash_value(&mut root, &stats.seal_ops);
1167 fnv1a_hash_value(&mut root, &stats.consolidation.runs_completed);
1168
1169 let root = manager.cached_physical_root(root, || {
1170 manager.for_each_entity_in_id_order(|entity| {
1171 record_physical_root_row_scan(&name);
1172 fnv1a_hash_value(&mut root, &entity.id.raw());
1173 fnv1a_hash_value(&mut root, &entity.kind);
1174 fnv1a_hash_value(&mut root, &entity.created_at);
1175 fnv1a_hash_value(&mut root, &entity.updated_at);
1176 fnv1a_hash_value(&mut root, &entity.data);
1177 fnv1a_hash_value(&mut root, &entity.sequence_id);
1178 fnv1a_hash_value(&mut root, &entity.embeddings().len());
1179 fnv1a_hash_value(&mut root, &entity.cross_refs().len());
1180 true
1181 });
1182 root
1183 });
1184
1185 roots.insert(name, root);
1186 }
1187
1188 roots
1189 }
1190
1191 pub fn table_ref(&self, table: impl Into<String>, row_id: u64) -> TableRef {
1197 TableRef::new(table, row_id)
1198 }
1199
1200 pub fn node_ref(&self, collection: impl Into<String>, node_id: EntityId) -> NodeRef {
1202 NodeRef::new(collection, node_id)
1203 }
1204
1205 pub fn vector_ref(&self, collection: impl Into<String>, vector_id: EntityId) -> VectorRef {
1207 VectorRef::new(collection, vector_id)
1208 }
1209
1210 pub fn query(&self) -> QueryBuilder {
1216 QueryBuilder::new(self.store.clone())
1217 }
1218
1219 pub fn similar(&self, collection: &str, vector: &[f32], k: usize) -> Vec<SimilarResult> {
1226 if self.store.get_collection(collection).is_none() {
1227 return Vec::new();
1228 }
1229
1230 if let Some(index) = self.get_or_build_hnsw_index(collection, vector.len()) {
1232 let hnsw = index.read().unwrap_or_else(|e| e.into_inner());
1233 let results = hnsw.search(vector, k);
1234 let mapped = self.hnsw_results_to_similar(collection, &results);
1235 if !mapped.is_empty() {
1236 return mapped;
1237 }
1238 }
1239
1240 self.similar_brute_force(collection, vector, k)
1242 }
1243
1244 fn similar_brute_force(
1246 &self,
1247 collection: &str,
1248 vector: &[f32],
1249 k: usize,
1250 ) -> Vec<SimilarResult> {
1251 let manager = match self.store.get_collection(collection) {
1252 Some(m) => m,
1253 None => return Vec::new(),
1254 };
1255
1256 let entities = manager.query_all(|_| true);
1257 let mut results: Vec<SimilarResult> = entities
1258 .iter()
1259 .filter_map(|e| {
1260 let score = match &e.data {
1261 EntityData::Vector(v) => cosine_similarity(vector, &v.dense),
1262 _ => e
1263 .embeddings()
1264 .iter()
1265 .map(|emb| cosine_similarity(vector, &emb.vector))
1266 .fold(0.0f32, f32::max),
1267 };
1268 let distance = (1.0 - score).max(0.0);
1269 if score > 0.0 {
1270 Some(SimilarResult {
1271 entity_id: e.id,
1272 score,
1273 distance,
1274 entity: e.clone(),
1275 })
1276 } else {
1277 None
1278 }
1279 })
1280 .collect();
1281
1282 results.sort_by(|a, b| {
1283 b.score
1284 .partial_cmp(&a.score)
1285 .unwrap_or(std::cmp::Ordering::Equal)
1286 });
1287 results.truncate(k);
1288 results
1289 }
1290
1291 fn get_or_build_hnsw_index(
1301 &self,
1302 collection: &str,
1303 query_dim: usize,
1304 ) -> Option<Arc<RwLock<HnswIndex>>> {
1305 let manager = self.store.get_collection(collection)?;
1306 let live_count = manager.count();
1307
1308 {
1310 let indexes = self
1311 .vector_indexes
1312 .read()
1313 .unwrap_or_else(|e| e.into_inner());
1314 if let Some(cached) = indexes.get(collection) {
1315 if cached.entity_count == live_count {
1316 return Some(Arc::clone(&cached.index));
1317 }
1318 }
1319 }
1320
1321 let entities = manager.query_all(|_| true);
1323
1324 let vectors: Vec<(u64, Vec<f32>)> = entities
1325 .iter()
1326 .filter_map(|e| match &e.data {
1327 EntityData::Vector(v) if !v.dense.is_empty() && v.dense.len() == query_dim => {
1328 Some((e.id.raw(), v.dense.clone()))
1329 }
1330 _ => None,
1331 })
1332 .collect();
1333
1334 const MIN_VECTORS_FOR_HNSW: usize = 100;
1336 if vectors.len() < MIN_VECTORS_FOR_HNSW {
1337 return None;
1338 }
1339
1340 let config = crate::storage::engine::HnswConfig::with_m(16)
1342 .with_metric(crate::storage::engine::DistanceMetric::Cosine)
1343 .with_ef_construction(100)
1344 .with_ef_search(50);
1345 let mut hnsw = HnswIndex::new(query_dim, config);
1346
1347 for (id, vec) in &vectors {
1348 hnsw.insert_with_id(*id, vec.clone());
1349 }
1350
1351 let index = Arc::new(RwLock::new(hnsw));
1352
1353 let mut indexes = self
1355 .vector_indexes
1356 .write()
1357 .unwrap_or_else(|e| e.into_inner());
1358 if let Some(cached) = indexes.get(collection) {
1360 if cached.entity_count == live_count {
1361 return Some(Arc::clone(&cached.index));
1362 }
1363 }
1364 indexes.insert(
1365 collection.to_string(),
1366 CachedVectorIndex {
1367 index: Arc::clone(&index),
1368 entity_count: live_count,
1369 },
1370 );
1371 Some(index)
1372 }
1373
1374 fn hnsw_results_to_similar(
1376 &self,
1377 collection: &str,
1378 results: &[crate::storage::engine::DistanceResult],
1379 ) -> Vec<SimilarResult> {
1380 results
1381 .iter()
1382 .filter_map(|dr| {
1383 let entity_id = EntityId::new(dr.id);
1384 let entity = self.store.get(collection, entity_id)?;
1385 let score = (1.0 - dr.distance).max(0.0);
1387 if score > 0.0 {
1388 Some(SimilarResult {
1389 entity_id,
1390 score,
1391 distance: dr.distance,
1392 entity,
1393 })
1394 } else {
1395 None
1396 }
1397 })
1398 .collect()
1399 }
1400
1401 pub(crate) fn invalidate_vector_index(&self, collection: &str) {
1406 let mut indexes = self
1407 .vector_indexes
1408 .write()
1409 .unwrap_or_else(|e| e.into_inner());
1410 indexes.remove(collection);
1411 }
1412
1413 pub fn get(&self, id: EntityId) -> Option<UnifiedEntity> {
1415 self.store.get_any(id).map(|(_, e)| e)
1416 }
1417
1418 pub fn get_with_collection(&self, id: EntityId) -> Option<(String, UnifiedEntity)> {
1420 self.store.get_any(id)
1421 }
1422
1423 pub fn batch_get(&self, ids: &[EntityId]) -> Vec<Option<UnifiedEntity>> {
1429 ids.iter().map(|id| self.get(*id)).collect()
1430 }
1431
1432 pub fn batch(&self) -> BatchBuilder {
1434 BatchBuilder::new(self.store.clone(), self.preprocessors.clone())
1435 }
1436
1437 pub fn add_preprocessor(&mut self, preprocessor: Box<dyn Preprocessor>) {
1443 let mut preprocessors = self
1444 .preprocessors
1445 .write()
1446 .unwrap_or_else(|poisoned| poisoned.into_inner());
1447 preprocessors.push(Arc::from(preprocessor));
1448 }
1449
1450 pub fn linked_from(&self, id: EntityId) -> Vec<LinkedEntity> {
1456 self.store
1457 .get_refs_from(id)
1458 .into_iter()
1459 .filter_map(|(target_id, ref_type, collection)| {
1460 self.store
1461 .get(&collection, target_id)
1462 .map(|entity| LinkedEntity {
1463 entity,
1464 ref_type,
1465 collection,
1466 })
1467 })
1468 .collect()
1469 }
1470
1471 pub fn linked_to(&self, id: EntityId) -> Vec<LinkedEntity> {
1473 self.store
1474 .get_refs_to(id)
1475 .into_iter()
1476 .filter_map(|(source_id, ref_type, collection)| {
1477 self.store
1478 .get(&collection, source_id)
1479 .map(|entity| LinkedEntity {
1480 entity,
1481 ref_type,
1482 collection,
1483 })
1484 })
1485 .collect()
1486 }
1487
1488 pub fn store(&self) -> Arc<UnifiedStore> {
1490 self.store.clone()
1491 }
1492
1493 pub(crate) fn turbo_collections(
1499 &self,
1500 ) -> &Arc<
1501 parking_lot::Mutex<
1502 std::collections::HashMap<
1503 String,
1504 Arc<crate::runtime::vector_turbo_kind::TurboCollectionState>,
1505 >,
1506 >,
1507 > {
1508 self.turbo_collections
1509 .get_or_init(|| Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new())))
1510 }
1511
1512 pub(crate) fn turbo_state(
1522 &self,
1523 collection: &str,
1524 ) -> Option<Arc<crate::runtime::vector_turbo_kind::TurboCollectionState>> {
1525 if !crate::runtime::vector_turbo_kind::is_turbo(&self.store, collection) {
1526 return None;
1527 }
1528 let map = self.turbo_collections();
1529 {
1530 let guard = map.lock();
1531 if let Some(state) = guard.get(collection) {
1532 return Some(Arc::clone(state));
1533 }
1534 }
1535 let contract = self.collection_contract(collection)?;
1536 let dim = contract.vector_dimension?;
1537 let metric = contract
1538 .vector_metric
1539 .unwrap_or(crate::storage::engine::distance::DistanceMetric::Cosine);
1540 let state = Arc::new(
1541 crate::runtime::vector_turbo_kind::TurboCollectionState::new(
1542 dim,
1543 metric,
1544 self.store.pager(),
1545 ),
1546 );
1547 let mut guard = map.lock();
1550 let inserted_now = !guard.contains_key(collection);
1551 let entry = guard
1552 .entry(collection.to_string())
1553 .or_insert_with(|| Arc::clone(&state));
1554 let handle = Arc::clone(entry);
1555 drop(guard);
1556 if inserted_now {
1563 let join = crate::runtime::vector_turbo_kind::spawn_background_rebuild(
1564 Arc::clone(&self.store),
1565 collection.to_string(),
1566 Arc::clone(&handle),
1567 );
1568 self.turbo_rebuild_workers.lock().push(join);
1569 }
1570 Some(handle)
1571 }
1572
1573 pub fn ml_runtime(&self) -> &crate::storage::ml::MlRuntime {
1579 self.ml_runtime.get_or_init(|| {
1580 crate::storage::ml::MlRuntime::in_memory(std::sync::Arc::new(
1581 |_handle| Ok(String::new()),
1585 ))
1586 })
1587 }
1588
1589 pub fn semantic_cache(&self) -> &Arc<crate::storage::ml::SemanticCache> {
1593 self.semantic_cache.get_or_init(|| {
1594 Arc::new(crate::storage::ml::SemanticCache::new(
1595 crate::storage::ml::SemanticCacheConfig::default(),
1596 ))
1597 })
1598 }
1599
1600 pub fn hypertables(&self) -> &Arc<crate::storage::timeseries::HypertableRegistry> {
1603 self.hypertables
1604 .get_or_init(|| Arc::new(crate::storage::timeseries::HypertableRegistry::new()))
1605 }
1606
1607 pub fn continuous_aggregates(
1610 &self,
1611 ) -> &Arc<crate::storage::timeseries::continuous_aggregate::ContinuousAggregateEngine> {
1612 self.continuous_aggregates.get_or_init(|| {
1613 Arc::new(
1614 crate::storage::timeseries::continuous_aggregate::ContinuousAggregateEngine::new(),
1615 )
1616 })
1617 }
1618
1619 pub(crate) fn is_binary_dump(path: &Path) -> Result<bool, std::io::Error> {
1620 let mut file = File::open(path)?;
1621 let mut magic = [0u8; 4];
1622 let read = file.read(&mut magic)?;
1623 Ok(read == 4 && reddb_file::native_store_magic_matches(&magic))
1624 }
1625}
1626
1627#[cfg(not(test))]
1628fn record_physical_root_row_scan(_collection: &str) {}
1629
1630#[cfg(test)]
1631std::thread_local! {
1632 static PHYSICAL_ROOT_ROW_SCANS: std::cell::RefCell<std::collections::BTreeMap<String, u64>> =
1633 const { std::cell::RefCell::new(std::collections::BTreeMap::new()) };
1634}
1635
1636#[cfg(test)]
1637fn record_physical_root_row_scan(collection: &str) {
1638 PHYSICAL_ROOT_ROW_SCANS.with(|scans| {
1639 *scans
1640 .borrow_mut()
1641 .entry(collection.to_string())
1642 .or_default() += 1;
1643 });
1644}
1645
1646#[cfg(test)]
1647impl RedDB {
1648 pub(crate) fn reset_physical_root_row_scans_for_test(&self) {
1649 PHYSICAL_ROOT_ROW_SCANS.with(|scans| scans.borrow_mut().clear());
1650 }
1651
1652 pub(crate) fn physical_root_row_scans_for_test(
1653 &self,
1654 ) -> std::collections::BTreeMap<String, u64> {
1655 PHYSICAL_ROOT_ROW_SCANS.with(|scans| scans.borrow().clone())
1656 }
1657}
1658
1659#[cfg(test)]
1660mod artifact_memory_tests {
1661 use super::*;
1662
1663 #[test]
1664 fn physical_roots_hash_borrowed_payloads_without_rendered_copies() {
1665 fn legacy_root(db: &RedDB) -> u64 {
1666 fn hash<T: Debug>(root: &mut u64, value: &T) {
1667 fnv1a_hash_bytes(root, format!("{value:?}").as_bytes());
1668 }
1669 let manager = db.store().get_collection("root_source").expect("source");
1670 let stats = manager.stats();
1671 let mut root = fnv1a_seed();
1672 hash(&mut root, &"root_source".to_string());
1673 hash(&mut root, &stats.total_entities);
1674 hash(&mut root, &stats.growing_count);
1675 hash(&mut root, &stats.sealed_count);
1676 hash(&mut root, &stats.archived_count);
1677 hash(&mut root, &stats.total_memory_bytes);
1678 hash(&mut root, &stats.seal_ops);
1679 hash(&mut root, &stats.consolidation.runs_completed);
1680 manager.for_each_entity_in_id_order(|entity| {
1681 hash(&mut root, &entity.id.raw());
1682 hash(&mut root, &entity.kind);
1683 hash(&mut root, &entity.created_at);
1684 hash(&mut root, &entity.updated_at);
1685 hash(&mut root, &entity.data);
1686 hash(&mut root, &entity.sequence_id);
1687 hash(&mut root, &entity.embeddings().len());
1688 hash(&mut root, &entity.cross_refs().len());
1689 true
1690 });
1691 root
1692 }
1693
1694 let runtime =
1695 crate::RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
1696 runtime
1697 .execute_query("CREATE TABLE root_source (payload BLOB, label TEXT, body JSON)")
1698 .expect("table");
1699 let db = runtime.db();
1700 let manager = db.store().get_collection("root_source").expect("source");
1701 let mut entity = UnifiedEntity::table_row(
1702 EntityId::new(1001),
1703 "root_source",
1704 1,
1705 vec![
1706 Value::Blob(vec![255; 2 * 1024 * 1024]),
1707 Value::text("escaped\n\t\0\"\\ and Unicode ç🦀"),
1708 Value::Json(br#"{"nested":[null,true,17,"kept"]}"#.to_vec()),
1709 ],
1710 );
1711 manager.insert(entity.clone()).expect("source row");
1712 let expected = legacy_root(&db);
1713 let (roots, measured) = crate::storage::no_malloc_hot_paths::measure_allocations(|| {
1714 db.physical_collection_roots()
1715 });
1716 assert_eq!(roots["root_source"], expected, "retain legacy hash bytes");
1717 eprintln!(
1718 "physical root allocation bytes: {}",
1719 measured.allocated_bytes
1720 );
1721 assert!(
1722 measured.allocated_bytes < 64 * 1024,
1723 "fingerprinting must not render the borrowed payload: {measured:?}"
1724 );
1725 assert_eq!(db.physical_collection_roots(), roots, "unchanged cache");
1726 let EntityData::Row(row) = &mut entity.data else {
1727 unreachable!("fixture is a table row");
1728 };
1729 row.columns[0] = Value::Blob(vec![254; 2 * 1024 * 1024]);
1730 manager
1731 .update(entity)
1732 .expect("replace payload of equal size");
1733 let updated = db.physical_collection_roots();
1734 assert_ne!(updated["root_source"], roots["root_source"]);
1735 assert_eq!(updated["root_source"], legacy_root(&db));
1736 }
1737
1738 #[test]
1739 fn document_artifact_paths_survive_shared_recovery_shapes() {
1740 let mut entity = UnifiedEntity::table_row(EntityId::new(1), "documents", 1, vec![]);
1741 entity.data = EntityData::Row(crate::storage::unified::entity::RowData::with_names(
1742 vec![Value::Json(br#"{"thing":"kept"}"#.to_vec())],
1743 vec!["body".into()],
1744 ));
1745 let before = RedDB::native_document_pathvalue_for_entity(entity.id, &entity.data, 4096)
1746 .expect("named document");
1747 let EntityData::Row(row) = &mut entity.data else {
1748 unreachable!("fixture is a row");
1749 };
1750 *row = crate::storage::unified::entity::RowData {
1751 columns: vec![row.get_field("body").expect("body field").clone()],
1752 named: None,
1753 schema: Some(Arc::new(vec!["body".into()])),
1754 };
1755 let after = RedDB::native_document_pathvalue_for_entity(entity.id, &entity.data, 4096)
1756 .expect("shared document");
1757 assert_eq!(
1758 before, after,
1759 "compaction must preserve named document paths"
1760 );
1761 }
1762
1763 #[test]
1764 fn disabled_fulltext_does_not_copy_rows_or_publish_search_artifacts() {
1765 let runtime =
1766 crate::RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
1767 runtime
1768 .execute_query("CREATE TABLE source (payload BLOB)")
1769 .expect("table");
1770 let db = runtime.db();
1771 let manager = db.store().get_collection("source").expect("manager");
1772 let (_, baseline) = crate::storage::no_malloc_hot_paths::measure_allocations(|| {
1773 db.native_vector_artifact_records().expect("empty build")
1774 });
1775 for id in 1..=32 {
1776 manager
1777 .insert(UnifiedEntity::table_row(
1778 EntityId::new(1000 + id),
1779 "source",
1780 id,
1781 vec![Value::Blob(vec![255; 64 * 1024])],
1782 ))
1783 .expect("source row");
1784 }
1785 let (build, measured) = crate::storage::no_malloc_hot_paths::measure_allocations(|| {
1786 db.native_vector_artifact_records().expect("disabled build")
1787 });
1788 assert!(build.records.is_empty());
1789 assert!(
1790 build.deferred_collections.is_empty(),
1791 "disabled capabilities do not consume artifact allowance"
1792 );
1793 assert!(
1794 measured.allocated_bytes <= baseline.allocated_bytes + 4096,
1795 "disabled fulltext copied source payloads: {baseline:?} -> {measured:?}"
1796 );
1797 }
1798
1799 #[test]
1800 fn document_artifact_traversal_is_ordered_and_bounded() {
1801 let json = crate::json::from_slice::<JsonValue>(br#"{"a":{"b":1},"z":[2,3]}"#)
1802 .expect("JSON fixture");
1803 let mut entries = Vec::new();
1804 let mut bytes = 0;
1805 RedDB::collect_native_document_entries_from_json(
1806 "body",
1807 &json,
1808 &mut entries,
1809 &mut bytes,
1810 4096,
1811 )
1812 .expect("small document fits");
1813 assert_eq!(
1814 entries,
1815 vec![
1816 ("body.a.b".into(), "1".into()),
1817 ("body.z[0]".into(), "2".into()),
1818 ("body.z[1]".into(), "3".into())
1819 ]
1820 );
1821 let mut deep = JsonValue::Integer(1);
1822 for _ in 0..64 {
1823 deep = JsonValue::Object([("child".to_string(), deep)].into());
1824 }
1825 let mut entries = Vec::new();
1826 let mut bytes = 0;
1827 assert!(RedDB::collect_native_document_entries_from_json(
1828 "body",
1829 &deep,
1830 &mut entries,
1831 &mut bytes,
1832 128
1833 )
1834 .is_err());
1835 assert!(bytes <= 128);
1836 }
1837}