Skip to main content

velesdb_core/collection/
any_collection.rs

1//! Type-erased collection handle for callers that don't know the collection type.
2//!
3//! `AnyCollection` wraps the three typed collections in an enum, dispatching
4//! common operations via match arms. Zero-cost: no heap allocation, no vtable.
5//!
6//! # Variant access
7//!
8//! Three complementary APIs follow the std `Result` / `Option` / `Any` idiom:
9//!
10//! | Need                              | Method                               | Returns                            |
11//! |-----------------------------------|--------------------------------------|------------------------------------|
12//! | Check variant                     | [`is_vector`], [`is_graph`], …       | `bool`                             |
13//! | Borrow variant (shared)           | [`as_vector`], [`as_graph`], …       | `Option<&T>`                       |
14//! | Borrow variant (exclusive)        | [`as_vector_mut`], …                 | `Option<&mut T>`                   |
15//! | Consume with recovery on miss     | [`into_vector`], [`into_graph`], …   | `Result<T, Self>`                  |
16//!
17//! [`is_vector`]: AnyCollection::is_vector
18//! [`is_graph`]: AnyCollection::is_graph
19//! [`as_vector`]: AnyCollection::as_vector
20//! [`as_graph`]: AnyCollection::as_graph
21//! [`as_vector_mut`]: AnyCollection::as_vector_mut
22//! [`into_vector`]: AnyCollection::into_vector
23//! [`into_graph`]: AnyCollection::into_graph
24
25use std::collections::HashMap;
26
27use crate::collection::graph_collection::GraphCollection;
28use crate::collection::metadata_collection::MetadataCollection;
29use crate::collection::types::CollectionConfig;
30use crate::collection::vector_collection::VectorCollection;
31use crate::error::Result;
32use crate::point::SearchResult;
33
34/// Type-erased collection handle for callers that don't know the collection type.
35///
36/// Dispatches common operations to the inner typed collection via enum match.
37/// Zero-cost: no heap allocation, no vtable — just a match arm per variant.
38///
39/// # Examples
40///
41/// ```rust,no_run
42/// use velesdb_core::{AnyCollection, Database};
43///
44/// let db = Database::open("./data")?;
45/// if let Some(any) = db.get_any_collection("docs") {
46///     // `config()`, `flush()`, `point_count()`, `name()`, `execute_query_str()`
47///     // dispatch across all variants — safe on every kind.
48///     println!("{}: {} pts", any.name(), any.point_count());
49///
50///     // Pattern-match when a variant-specific method is needed.
51///     match &any {
52///         AnyCollection::Vector(_) => println!("vector collection"),
53///         AnyCollection::Graph(_)  => println!("graph collection"),
54///         AnyCollection::Metadata(_) => println!("metadata collection"),
55///         _ => println!("unknown variant"),
56///     }
57/// }
58/// # Ok::<(), velesdb_core::Error>(())
59/// ```
60#[derive(Clone)]
61#[non_exhaustive]
62pub enum AnyCollection {
63    /// A vector collection (HNSW + payload + full-text).
64    Vector(VectorCollection),
65    /// A graph collection (edges + optional node embeddings).
66    Graph(GraphCollection),
67    /// A metadata-only collection (payload, no vectors).
68    Metadata(MetadataCollection),
69}
70
71impl AnyCollection {
72    // -------------------------------------------------------------------------
73    // Shared operations (dispatch on variant)
74    // -------------------------------------------------------------------------
75
76    /// The shared inner [`Collection`](crate::collection::types::Collection)
77    /// backing every variant. All three newtypes wrap the same `Collection`, so
78    /// operations that are identical across kinds dispatch through this one
79    /// accessor instead of a per-variant match — removing the ad-hoc mix of
80    /// `c.method()` / `c.inner.method()` forwarding that the audit flagged (P2.6).
81    #[inline]
82    fn inner(&self) -> &crate::collection::types::Collection {
83        match self {
84            Self::Vector(c) => &c.inner,
85            Self::Graph(c) => &c.inner,
86            Self::Metadata(c) => &c.inner,
87        }
88    }
89
90    /// Returns the collection configuration.
91    #[must_use]
92    pub fn config(&self) -> CollectionConfig {
93        self.inner().config()
94    }
95
96    /// Flushes all state to disk.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if any flush operation fails.
101    pub fn flush(&self) -> Result<()> {
102        match self {
103            Self::Vector(c) => c.flush(),
104            Self::Graph(c) => c.flush(),
105            Self::Metadata(c) => c.flush(),
106        }
107    }
108
109    /// Returns the number of points in the collection.
110    #[must_use]
111    pub fn point_count(&self) -> usize {
112        self.config().point_count
113    }
114
115    /// Returns `true` if the collection contains no points.
116    #[must_use]
117    pub fn is_empty(&self) -> bool {
118        self.inner().is_empty()
119    }
120
121    /// Returns `true` if this is a metadata-only collection.
122    ///
123    /// Equivalent to [`is_metadata`](Self::is_metadata) — kept for backward
124    /// compatibility with older call sites.
125    #[must_use]
126    pub fn is_metadata_only(&self) -> bool {
127        matches!(self, Self::Metadata(_))
128    }
129
130    /// Returns the collection name.
131    #[must_use]
132    pub fn name(&self) -> String {
133        self.config().name
134    }
135
136    /// Executes a raw VelesQL string, parsing it before execution.
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if parsing or execution fails.
141    pub fn execute_query_str(
142        &self,
143        sql: &str,
144        params: &HashMap<String, serde_json::Value>,
145    ) -> Result<Vec<SearchResult>> {
146        match self {
147            Self::Vector(c) => c.execute_query_str(sql, params),
148            Self::Graph(c) => c.execute_query_str(sql, params),
149            Self::Metadata(c) => c.execute_query_str(sql, params),
150        }
151    }
152
153    /// Executes an aggregation query (GROUP BY / COUNT / SUM / AVG / MIN / MAX).
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if the query is invalid or aggregation computation fails.
158    pub fn execute_aggregate(
159        &self,
160        query: &crate::velesql::Query,
161        params: &HashMap<String, serde_json::Value>,
162    ) -> Result<serde_json::Value> {
163        self.inner().execute_aggregate(query, params)
164    }
165
166    /// Returns collection diagnostics.
167    #[must_use]
168    pub fn diagnostics(&self) -> crate::collection::CollectionDiagnostics {
169        match self {
170            Self::Vector(c) => c.diagnostics(),
171            Self::Graph(c) => c.diagnostics(),
172            Self::Metadata(c) => c.diagnostics(),
173        }
174    }
175
176    // -------------------------------------------------------------------------
177    // Graph edge operations (shared across all collection types)
178    // -------------------------------------------------------------------------
179
180    /// Adds a graph edge.
181    ///
182    /// # Errors
183    ///
184    /// Returns an error if the edge cannot be stored.
185    pub fn add_edge(&self, edge: crate::collection::graph::GraphEdge) -> Result<()> {
186        self.inner().add_edge(edge)
187    }
188
189    /// Removes a graph edge by ID. Returns `true` if the edge existed.
190    #[must_use]
191    pub fn remove_edge(&self, edge_id: u64) -> bool {
192        self.inner().remove_edge(edge_id)
193    }
194
195    /// Returns outgoing edges from a node.
196    #[must_use]
197    pub fn get_outgoing_edges(&self, node_id: u64) -> Vec<crate::collection::graph::GraphEdge> {
198        self.inner().get_outgoing_edges(node_id)
199    }
200
201    /// Returns the highest edge ID in the graph, if any.
202    #[must_use]
203    pub fn max_edge_id(&self) -> Option<u64> {
204        self.inner().max_edge_id()
205    }
206
207    /// Returns `true` when an edge with `edge_id` exists.
208    #[must_use]
209    pub fn edge_exists(&self, edge_id: u64) -> bool {
210        self.inner().edge_exists(edge_id)
211    }
212
213    // -------------------------------------------------------------------------
214    // Point retrieval (shared)
215    // -------------------------------------------------------------------------
216
217    /// Retrieves points by IDs, returning `None` for missing entries.
218    #[must_use]
219    pub fn get(&self, ids: &[u64]) -> Vec<Option<crate::point::Point>> {
220        match self {
221            Self::Vector(c) => c.get(ids),
222            Self::Graph(c) => c.get(ids),
223            Self::Metadata(c) => c.get(ids),
224        }
225    }
226
227    /// Upserts points (vector + payload).
228    ///
229    /// For graph collections the payload is stored via the node-payload path
230    /// (no vector update occurs since graph nodes have no embedding by default).
231    ///
232    /// # Errors
233    ///
234    /// Returns an error if storage fails.
235    pub fn upsert(&self, points: Vec<crate::point::Point>) -> Result<()> {
236        match self {
237            Self::Vector(c) => c.upsert(points),
238            Self::Graph(c) => {
239                // One barrier for the batch, matching the Vector and Metadata
240                // arms. This used to loop the single-node path, paying an
241                // fsync, an auto-snapshot check and two label-index
242                // acquisitions per node (#2153).
243                let entries: Vec<(u64, &serde_json::Value)> = points
244                    .iter()
245                    .filter_map(|p| p.payload.as_ref().map(|payload| (p.id, payload)))
246                    .collect();
247                c.upsert_node_payloads(&entries)
248            }
249            Self::Metadata(c) => c.upsert(points),
250        }
251    }
252
253    // -------------------------------------------------------------------------
254    // Variant discriminants (`is_*`)
255    // -------------------------------------------------------------------------
256
257    /// Returns `true` if this collection is the [`Vector`](Self::Vector) variant.
258    ///
259    /// # Examples
260    ///
261    /// ```rust,no_run
262    /// use velesdb_core::{AnyCollection, Database, DistanceMetric};
263    ///
264    /// let db = Database::open("./data")?;
265    /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
266    /// let any = db.get_any_collection("docs").expect("exists");
267    /// assert!(any.is_vector());
268    /// assert!(!any.is_graph());
269    /// # Ok::<(), velesdb_core::Error>(())
270    /// ```
271    #[must_use]
272    pub fn is_vector(&self) -> bool {
273        matches!(self, Self::Vector(_))
274    }
275
276    /// Returns `true` if this collection is the [`Graph`](Self::Graph) variant.
277    ///
278    /// # Examples
279    ///
280    /// ```rust,no_run
281    /// use velesdb_core::{Database, GraphSchema};
282    ///
283    /// let db = Database::open("./data")?;
284    /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
285    /// let any = db.get_any_collection("edges").expect("exists");
286    /// assert!(any.is_graph());
287    /// # Ok::<(), velesdb_core::Error>(())
288    /// ```
289    #[must_use]
290    pub fn is_graph(&self) -> bool {
291        matches!(self, Self::Graph(_))
292    }
293
294    /// Returns `true` if this collection is the [`Metadata`](Self::Metadata) variant.
295    ///
296    /// # Examples
297    ///
298    /// ```rust,no_run
299    /// use velesdb_core::Database;
300    ///
301    /// let db = Database::open("./data")?;
302    /// db.create_metadata_collection("catalog")?;
303    /// let any = db.get_any_collection("catalog").expect("exists");
304    /// assert!(any.is_metadata());
305    /// # Ok::<(), velesdb_core::Error>(())
306    /// ```
307    #[must_use]
308    pub fn is_metadata(&self) -> bool {
309        matches!(self, Self::Metadata(_))
310    }
311
312    // -------------------------------------------------------------------------
313    // Shared borrows (`as_*`) — zero-cost, return `Option<&T>`
314    // -------------------------------------------------------------------------
315
316    /// Returns a shared reference to the inner [`VectorCollection`] if this is
317    /// the [`Vector`](Self::Vector) variant, or `None` otherwise.
318    ///
319    /// # Examples
320    ///
321    /// ```rust,no_run
322    /// use velesdb_core::{Database, DistanceMetric};
323    ///
324    /// let db = Database::open("./data")?;
325    /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
326    /// let any = db.get_any_collection("docs").expect("exists");
327    /// if let Some(v) = any.as_vector() {
328    ///     let _ = v.config().dimension;
329    /// }
330    /// # Ok::<(), velesdb_core::Error>(())
331    /// ```
332    #[must_use]
333    pub fn as_vector(&self) -> Option<&VectorCollection> {
334        match self {
335            Self::Vector(c) => Some(c),
336            _ => None,
337        }
338    }
339
340    /// Returns an exclusive reference to the inner [`VectorCollection`] if
341    /// this is the [`Vector`](Self::Vector) variant, or `None` otherwise.
342    #[must_use]
343    pub fn as_vector_mut(&mut self) -> Option<&mut VectorCollection> {
344        match self {
345            Self::Vector(c) => Some(c),
346            _ => None,
347        }
348    }
349
350    /// Returns a shared reference to the inner [`GraphCollection`] if this is
351    /// the [`Graph`](Self::Graph) variant, or `None` otherwise.
352    ///
353    /// # Examples
354    ///
355    /// ```rust,no_run
356    /// use velesdb_core::{Database, GraphSchema};
357    ///
358    /// let db = Database::open("./data")?;
359    /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
360    /// let any = db.get_any_collection("edges").expect("exists");
361    /// if let Some(g) = any.as_graph() {
362    ///     let _ = g.edge_count();
363    /// }
364    /// # Ok::<(), velesdb_core::Error>(())
365    /// ```
366    #[must_use]
367    pub fn as_graph(&self) -> Option<&GraphCollection> {
368        match self {
369            Self::Graph(c) => Some(c),
370            _ => None,
371        }
372    }
373
374    /// Returns an exclusive reference to the inner [`GraphCollection`] if
375    /// this is the [`Graph`](Self::Graph) variant, or `None` otherwise.
376    #[must_use]
377    pub fn as_graph_mut(&mut self) -> Option<&mut GraphCollection> {
378        match self {
379            Self::Graph(c) => Some(c),
380            _ => None,
381        }
382    }
383
384    /// Returns a shared reference to the inner [`MetadataCollection`] if this
385    /// is the [`Metadata`](Self::Metadata) variant, or `None` otherwise.
386    ///
387    /// # Examples
388    ///
389    /// ```rust,no_run
390    /// use velesdb_core::Database;
391    ///
392    /// let db = Database::open("./data")?;
393    /// db.create_metadata_collection("catalog")?;
394    /// let any = db.get_any_collection("catalog").expect("exists");
395    /// if let Some(m) = any.as_metadata() {
396    ///     let _ = m.is_empty();
397    /// }
398    /// # Ok::<(), velesdb_core::Error>(())
399    /// ```
400    #[must_use]
401    pub fn as_metadata(&self) -> Option<&MetadataCollection> {
402        match self {
403            Self::Metadata(c) => Some(c),
404            _ => None,
405        }
406    }
407
408    /// Returns an exclusive reference to the inner [`MetadataCollection`] if
409    /// this is the [`Metadata`](Self::Metadata) variant, or `None` otherwise.
410    #[must_use]
411    pub fn as_metadata_mut(&mut self) -> Option<&mut MetadataCollection> {
412        match self {
413            Self::Metadata(c) => Some(c),
414            _ => None,
415        }
416    }
417
418    // -------------------------------------------------------------------------
419    // Consuming conversions (`into_*`) — return `Result<T, Self>` for recovery
420    // -------------------------------------------------------------------------
421
422    /// Consumes `self` and returns the inner [`VectorCollection`] if this is
423    /// the [`Vector`](Self::Vector) variant.
424    ///
425    /// On the wrong variant, returns `Err(self)` so callers can recover
426    /// ownership — mirroring the std [`Result`] / [`TryFrom`] idiom.
427    ///
428    /// # Errors
429    ///
430    /// Returns the original `AnyCollection` unchanged when the variant is
431    /// [`Graph`](Self::Graph) or [`Metadata`](Self::Metadata).
432    ///
433    /// # Examples
434    ///
435    /// ```rust,no_run
436    /// use velesdb_core::{Database, DistanceMetric};
437    ///
438    /// let db = Database::open("./data")?;
439    /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
440    /// let any = db.get_any_collection("docs").expect("exists");
441    /// match any.into_vector() {
442    ///     Ok(v) => { let _ = v.config().dimension; }
443    ///     Err(original) => {
444    ///         // wrong variant; `original` still valid
445    ///         assert!(!original.is_vector());
446    ///     }
447    /// }
448    /// # Ok::<(), velesdb_core::Error>(())
449    /// ```
450    // `Err`-variant is `Self` by design — mirrors std `TryFrom` so callers
451    // recover ownership on the wrong variant. Box-wrapping would defeat the
452    // purpose and forces an allocation on every miss.
453    #[allow(clippy::result_large_err)]
454    pub fn into_vector(self) -> core::result::Result<VectorCollection, Self> {
455        match self {
456            Self::Vector(c) => Ok(c),
457            other => Err(other),
458        }
459    }
460
461    /// Consumes `self` and returns the inner [`GraphCollection`] if this is
462    /// the [`Graph`](Self::Graph) variant.
463    ///
464    /// On the wrong variant, returns `Err(self)` so callers can recover
465    /// ownership.
466    ///
467    /// # Errors
468    ///
469    /// Returns the original `AnyCollection` unchanged when the variant is
470    /// [`Vector`](Self::Vector) or [`Metadata`](Self::Metadata).
471    ///
472    /// # Examples
473    ///
474    /// ```rust,no_run
475    /// use velesdb_core::{Database, GraphSchema};
476    ///
477    /// let db = Database::open("./data")?;
478    /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
479    /// let any = db.get_any_collection("edges").expect("exists");
480    /// match any.into_graph() {
481    ///     Ok(graph) => { let _ = graph.edge_count(); }
482    ///     Err(_wrong_variant) => unreachable!("edges is a graph collection"),
483    /// }
484    /// # Ok::<(), velesdb_core::Error>(())
485    /// ```
486    #[allow(clippy::result_large_err)]
487    pub fn into_graph(self) -> core::result::Result<GraphCollection, Self> {
488        match self {
489            Self::Graph(c) => Ok(c),
490            other => Err(other),
491        }
492    }
493
494    /// Consumes `self` and returns the inner [`MetadataCollection`] if this
495    /// is the [`Metadata`](Self::Metadata) variant.
496    ///
497    /// On the wrong variant, returns `Err(self)` so callers can recover
498    /// ownership.
499    ///
500    /// # Errors
501    ///
502    /// Returns the original `AnyCollection` unchanged when the variant is
503    /// [`Vector`](Self::Vector) or [`Graph`](Self::Graph).
504    ///
505    /// # Examples
506    ///
507    /// ```rust,no_run
508    /// use velesdb_core::Database;
509    ///
510    /// let db = Database::open("./data")?;
511    /// db.create_metadata_collection("catalog")?;
512    /// let any = db.get_any_collection("catalog").expect("exists");
513    /// match any.into_metadata() {
514    ///     Ok(meta) => assert!(meta.is_empty()),
515    ///     Err(_wrong_variant) => unreachable!("catalog is a metadata collection"),
516    /// }
517    /// # Ok::<(), velesdb_core::Error>(())
518    /// ```
519    #[allow(clippy::result_large_err)]
520    pub fn into_metadata(self) -> core::result::Result<MetadataCollection, Self> {
521        match self {
522            Self::Metadata(c) => Ok(c),
523            other => Err(other),
524        }
525    }
526}