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