Skip to main content

reddb_server/storage/unified/devx/reddb/
impl_access.rs

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