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                for p in points {
240                    if let Some(payload) = p.payload.as_ref() {
241                        c.upsert_node_payload(p.id, payload)?;
242                    }
243                }
244                Ok(())
245            }
246            Self::Metadata(c) => c.upsert(points),
247        }
248    }
249
250    // -------------------------------------------------------------------------
251    // Variant discriminants (`is_*`)
252    // -------------------------------------------------------------------------
253
254    /// Returns `true` if this collection is the [`Vector`](Self::Vector) variant.
255    ///
256    /// # Examples
257    ///
258    /// ```rust,no_run
259    /// use velesdb_core::{AnyCollection, Database, DistanceMetric};
260    ///
261    /// let db = Database::open("./data")?;
262    /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
263    /// let any = db.get_any_collection("docs").expect("exists");
264    /// assert!(any.is_vector());
265    /// assert!(!any.is_graph());
266    /// # Ok::<(), velesdb_core::Error>(())
267    /// ```
268    #[must_use]
269    pub fn is_vector(&self) -> bool {
270        matches!(self, Self::Vector(_))
271    }
272
273    /// Returns `true` if this collection is the [`Graph`](Self::Graph) variant.
274    ///
275    /// # Examples
276    ///
277    /// ```rust,no_run
278    /// use velesdb_core::{Database, GraphSchema};
279    ///
280    /// let db = Database::open("./data")?;
281    /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
282    /// let any = db.get_any_collection("edges").expect("exists");
283    /// assert!(any.is_graph());
284    /// # Ok::<(), velesdb_core::Error>(())
285    /// ```
286    #[must_use]
287    pub fn is_graph(&self) -> bool {
288        matches!(self, Self::Graph(_))
289    }
290
291    /// Returns `true` if this collection is the [`Metadata`](Self::Metadata) variant.
292    ///
293    /// # Examples
294    ///
295    /// ```rust,no_run
296    /// use velesdb_core::Database;
297    ///
298    /// let db = Database::open("./data")?;
299    /// db.create_metadata_collection("catalog")?;
300    /// let any = db.get_any_collection("catalog").expect("exists");
301    /// assert!(any.is_metadata());
302    /// # Ok::<(), velesdb_core::Error>(())
303    /// ```
304    #[must_use]
305    pub fn is_metadata(&self) -> bool {
306        matches!(self, Self::Metadata(_))
307    }
308
309    // -------------------------------------------------------------------------
310    // Shared borrows (`as_*`) — zero-cost, return `Option<&T>`
311    // -------------------------------------------------------------------------
312
313    /// Returns a shared reference to the inner [`VectorCollection`] if this is
314    /// the [`Vector`](Self::Vector) variant, or `None` otherwise.
315    ///
316    /// # Examples
317    ///
318    /// ```rust,no_run
319    /// use velesdb_core::{Database, DistanceMetric};
320    ///
321    /// let db = Database::open("./data")?;
322    /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
323    /// let any = db.get_any_collection("docs").expect("exists");
324    /// if let Some(v) = any.as_vector() {
325    ///     let _ = v.config().dimension;
326    /// }
327    /// # Ok::<(), velesdb_core::Error>(())
328    /// ```
329    #[must_use]
330    pub fn as_vector(&self) -> Option<&VectorCollection> {
331        match self {
332            Self::Vector(c) => Some(c),
333            _ => None,
334        }
335    }
336
337    /// Returns an exclusive reference to the inner [`VectorCollection`] if
338    /// this is the [`Vector`](Self::Vector) variant, or `None` otherwise.
339    #[must_use]
340    pub fn as_vector_mut(&mut self) -> Option<&mut VectorCollection> {
341        match self {
342            Self::Vector(c) => Some(c),
343            _ => None,
344        }
345    }
346
347    /// Returns a shared reference to the inner [`GraphCollection`] if this is
348    /// the [`Graph`](Self::Graph) variant, or `None` otherwise.
349    ///
350    /// # Examples
351    ///
352    /// ```rust,no_run
353    /// use velesdb_core::{Database, GraphSchema};
354    ///
355    /// let db = Database::open("./data")?;
356    /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
357    /// let any = db.get_any_collection("edges").expect("exists");
358    /// if let Some(g) = any.as_graph() {
359    ///     let _ = g.edge_count();
360    /// }
361    /// # Ok::<(), velesdb_core::Error>(())
362    /// ```
363    #[must_use]
364    pub fn as_graph(&self) -> Option<&GraphCollection> {
365        match self {
366            Self::Graph(c) => Some(c),
367            _ => None,
368        }
369    }
370
371    /// Returns an exclusive reference to the inner [`GraphCollection`] if
372    /// this is the [`Graph`](Self::Graph) variant, or `None` otherwise.
373    #[must_use]
374    pub fn as_graph_mut(&mut self) -> Option<&mut GraphCollection> {
375        match self {
376            Self::Graph(c) => Some(c),
377            _ => None,
378        }
379    }
380
381    /// Returns a shared reference to the inner [`MetadataCollection`] if this
382    /// is the [`Metadata`](Self::Metadata) variant, or `None` otherwise.
383    ///
384    /// # Examples
385    ///
386    /// ```rust,no_run
387    /// use velesdb_core::Database;
388    ///
389    /// let db = Database::open("./data")?;
390    /// db.create_metadata_collection("catalog")?;
391    /// let any = db.get_any_collection("catalog").expect("exists");
392    /// if let Some(m) = any.as_metadata() {
393    ///     let _ = m.is_empty();
394    /// }
395    /// # Ok::<(), velesdb_core::Error>(())
396    /// ```
397    #[must_use]
398    pub fn as_metadata(&self) -> Option<&MetadataCollection> {
399        match self {
400            Self::Metadata(c) => Some(c),
401            _ => None,
402        }
403    }
404
405    /// Returns an exclusive reference to the inner [`MetadataCollection`] if
406    /// this is the [`Metadata`](Self::Metadata) variant, or `None` otherwise.
407    #[must_use]
408    pub fn as_metadata_mut(&mut self) -> Option<&mut MetadataCollection> {
409        match self {
410            Self::Metadata(c) => Some(c),
411            _ => None,
412        }
413    }
414
415    // -------------------------------------------------------------------------
416    // Consuming conversions (`into_*`) — return `Result<T, Self>` for recovery
417    // -------------------------------------------------------------------------
418
419    /// Consumes `self` and returns the inner [`VectorCollection`] if this is
420    /// the [`Vector`](Self::Vector) variant.
421    ///
422    /// On the wrong variant, returns `Err(self)` so callers can recover
423    /// ownership — mirroring the std [`Result`] / [`TryFrom`] idiom.
424    ///
425    /// # Errors
426    ///
427    /// Returns the original `AnyCollection` unchanged when the variant is
428    /// [`Graph`](Self::Graph) or [`Metadata`](Self::Metadata).
429    ///
430    /// # Examples
431    ///
432    /// ```rust,no_run
433    /// use velesdb_core::{Database, DistanceMetric};
434    ///
435    /// let db = Database::open("./data")?;
436    /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
437    /// let any = db.get_any_collection("docs").expect("exists");
438    /// match any.into_vector() {
439    ///     Ok(v) => { let _ = v.config().dimension; }
440    ///     Err(original) => {
441    ///         // wrong variant; `original` still valid
442    ///         assert!(!original.is_vector());
443    ///     }
444    /// }
445    /// # Ok::<(), velesdb_core::Error>(())
446    /// ```
447    // `Err`-variant is `Self` by design — mirrors std `TryFrom` so callers
448    // recover ownership on the wrong variant. Box-wrapping would defeat the
449    // purpose and forces an allocation on every miss.
450    #[allow(clippy::result_large_err)]
451    pub fn into_vector(self) -> core::result::Result<VectorCollection, Self> {
452        match self {
453            Self::Vector(c) => Ok(c),
454            other => Err(other),
455        }
456    }
457
458    /// Consumes `self` and returns the inner [`GraphCollection`] if this is
459    /// the [`Graph`](Self::Graph) variant.
460    ///
461    /// On the wrong variant, returns `Err(self)` so callers can recover
462    /// ownership.
463    ///
464    /// # Errors
465    ///
466    /// Returns the original `AnyCollection` unchanged when the variant is
467    /// [`Vector`](Self::Vector) or [`Metadata`](Self::Metadata).
468    ///
469    /// # Examples
470    ///
471    /// ```rust,no_run
472    /// use velesdb_core::{Database, GraphSchema};
473    ///
474    /// let db = Database::open("./data")?;
475    /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
476    /// let any = db.get_any_collection("edges").expect("exists");
477    /// match any.into_graph() {
478    ///     Ok(graph) => { let _ = graph.edge_count(); }
479    ///     Err(_wrong_variant) => unreachable!("edges is a graph collection"),
480    /// }
481    /// # Ok::<(), velesdb_core::Error>(())
482    /// ```
483    #[allow(clippy::result_large_err)]
484    pub fn into_graph(self) -> core::result::Result<GraphCollection, Self> {
485        match self {
486            Self::Graph(c) => Ok(c),
487            other => Err(other),
488        }
489    }
490
491    /// Consumes `self` and returns the inner [`MetadataCollection`] if this
492    /// is the [`Metadata`](Self::Metadata) variant.
493    ///
494    /// On the wrong variant, returns `Err(self)` so callers can recover
495    /// ownership.
496    ///
497    /// # Errors
498    ///
499    /// Returns the original `AnyCollection` unchanged when the variant is
500    /// [`Vector`](Self::Vector) or [`Graph`](Self::Graph).
501    ///
502    /// # Examples
503    ///
504    /// ```rust,no_run
505    /// use velesdb_core::Database;
506    ///
507    /// let db = Database::open("./data")?;
508    /// db.create_metadata_collection("catalog")?;
509    /// let any = db.get_any_collection("catalog").expect("exists");
510    /// match any.into_metadata() {
511    ///     Ok(meta) => assert!(meta.is_empty()),
512    ///     Err(_wrong_variant) => unreachable!("catalog is a metadata collection"),
513    /// }
514    /// # Ok::<(), velesdb_core::Error>(())
515    /// ```
516    #[allow(clippy::result_large_err)]
517    pub fn into_metadata(self) -> core::result::Result<MetadataCollection, Self> {
518        match self {
519            Self::Metadata(c) => Ok(c),
520            other => Err(other),
521        }
522    }
523}