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    /// Returns the collection configuration.
77    #[must_use]
78    pub fn config(&self) -> CollectionConfig {
79        match self {
80            Self::Vector(c) => c.config(),
81            Self::Graph(c) => c.inner.config(),
82            Self::Metadata(c) => c.inner.config(),
83        }
84    }
85
86    /// Flushes all state to disk.
87    ///
88    /// # Errors
89    ///
90    /// Returns an error if any flush operation fails.
91    pub fn flush(&self) -> Result<()> {
92        match self {
93            Self::Vector(c) => c.flush(),
94            Self::Graph(c) => c.flush(),
95            Self::Metadata(c) => c.flush(),
96        }
97    }
98
99    /// Returns the number of points in the collection.
100    #[must_use]
101    pub fn point_count(&self) -> usize {
102        self.config().point_count
103    }
104
105    /// Returns `true` if the collection contains no points.
106    #[must_use]
107    pub fn is_empty(&self) -> bool {
108        match self {
109            Self::Vector(c) => c.inner.is_empty(),
110            Self::Graph(c) => c.is_empty(),
111            Self::Metadata(c) => c.is_empty(),
112        }
113    }
114
115    /// Returns `true` if this is a metadata-only collection.
116    ///
117    /// Equivalent to [`is_metadata`](Self::is_metadata) — kept for backward
118    /// compatibility with older call sites.
119    #[must_use]
120    pub fn is_metadata_only(&self) -> bool {
121        matches!(self, Self::Metadata(_))
122    }
123
124    /// Returns the collection name.
125    #[must_use]
126    pub fn name(&self) -> String {
127        self.config().name
128    }
129
130    /// Executes a raw VelesQL string, parsing it before execution.
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if parsing or execution fails.
135    pub fn execute_query_str(
136        &self,
137        sql: &str,
138        params: &HashMap<String, serde_json::Value>,
139    ) -> Result<Vec<SearchResult>> {
140        match self {
141            Self::Vector(c) => c.execute_query_str(sql, params),
142            Self::Graph(c) => c.execute_query_str(sql, params),
143            Self::Metadata(c) => c.execute_query_str(sql, params),
144        }
145    }
146
147    /// Executes an aggregation query (GROUP BY / COUNT / SUM / AVG / MIN / MAX).
148    ///
149    /// # Errors
150    ///
151    /// Returns an error if the query is invalid or aggregation computation fails.
152    pub fn execute_aggregate(
153        &self,
154        query: &crate::velesql::Query,
155        params: &HashMap<String, serde_json::Value>,
156    ) -> Result<serde_json::Value> {
157        match self {
158            Self::Vector(c) => c.execute_aggregate(query, params),
159            Self::Graph(c) => c.inner.execute_aggregate(query, params),
160            Self::Metadata(c) => c.inner.execute_aggregate(query, params),
161        }
162    }
163
164    /// Returns collection diagnostics.
165    #[must_use]
166    pub fn diagnostics(&self) -> crate::collection::CollectionDiagnostics {
167        match self {
168            Self::Vector(c) => c.diagnostics(),
169            Self::Graph(c) => c.diagnostics(),
170            Self::Metadata(c) => c.diagnostics(),
171        }
172    }
173
174    // -------------------------------------------------------------------------
175    // Graph edge operations (shared across all collection types)
176    // -------------------------------------------------------------------------
177
178    /// Adds a graph edge.
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if the edge cannot be stored.
183    pub fn add_edge(&self, edge: crate::collection::graph::GraphEdge) -> Result<()> {
184        match self {
185            Self::Vector(c) => c.add_edge(edge),
186            Self::Graph(c) => c.add_edge(edge),
187            Self::Metadata(c) => c.inner.add_edge(edge),
188        }
189    }
190
191    /// Removes a graph edge by ID. Returns `true` if the edge existed.
192    #[must_use]
193    pub fn remove_edge(&self, edge_id: u64) -> bool {
194        match self {
195            Self::Vector(c) => c.remove_edge(edge_id),
196            Self::Graph(c) => c.remove_edge(edge_id),
197            Self::Metadata(c) => c.inner.remove_edge(edge_id),
198        }
199    }
200
201    /// Returns outgoing edges from a node.
202    #[must_use]
203    pub fn get_outgoing_edges(&self, node_id: u64) -> Vec<crate::collection::graph::GraphEdge> {
204        match self {
205            Self::Vector(c) => c.get_outgoing_edges(node_id),
206            Self::Graph(c) => c.get_outgoing(node_id),
207            Self::Metadata(c) => c.inner.get_outgoing_edges(node_id),
208        }
209    }
210
211    /// Returns the highest edge ID in the graph, if any.
212    #[must_use]
213    pub fn max_edge_id(&self) -> Option<u64> {
214        match self {
215            Self::Vector(c) => c.max_edge_id(),
216            Self::Graph(c) => c.inner.max_edge_id(),
217            Self::Metadata(c) => c.inner.max_edge_id(),
218        }
219    }
220
221    /// Returns `true` when an edge with `edge_id` exists.
222    #[must_use]
223    pub fn edge_exists(&self, edge_id: u64) -> bool {
224        match self {
225            Self::Vector(c) => c.edge_exists(edge_id),
226            Self::Graph(c) => c.inner.edge_exists(edge_id),
227            Self::Metadata(c) => c.inner.edge_exists(edge_id),
228        }
229    }
230
231    // -------------------------------------------------------------------------
232    // Point retrieval (shared)
233    // -------------------------------------------------------------------------
234
235    /// Retrieves points by IDs, returning `None` for missing entries.
236    #[must_use]
237    pub fn get(&self, ids: &[u64]) -> Vec<Option<crate::point::Point>> {
238        match self {
239            Self::Vector(c) => c.get(ids),
240            Self::Graph(c) => c.get(ids),
241            Self::Metadata(c) => c.get(ids),
242        }
243    }
244
245    /// Upserts points (vector + payload).
246    ///
247    /// For graph collections the payload is stored via the node-payload path
248    /// (no vector update occurs since graph nodes have no embedding by default).
249    ///
250    /// # Errors
251    ///
252    /// Returns an error if storage fails.
253    pub fn upsert(&self, points: Vec<crate::point::Point>) -> Result<()> {
254        match self {
255            Self::Vector(c) => c.upsert(points),
256            Self::Graph(c) => {
257                for p in points {
258                    if let Some(payload) = p.payload.as_ref() {
259                        c.upsert_node_payload(p.id, payload)?;
260                    }
261                }
262                Ok(())
263            }
264            Self::Metadata(c) => c.upsert(points),
265        }
266    }
267
268    // -------------------------------------------------------------------------
269    // Variant discriminants (`is_*`)
270    // -------------------------------------------------------------------------
271
272    /// Returns `true` if this collection is the [`Vector`](Self::Vector) variant.
273    ///
274    /// # Examples
275    ///
276    /// ```rust,no_run
277    /// use velesdb_core::{AnyCollection, Database, DistanceMetric};
278    ///
279    /// let db = Database::open("./data")?;
280    /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
281    /// let any = db.get_any_collection("docs").expect("exists");
282    /// assert!(any.is_vector());
283    /// assert!(!any.is_graph());
284    /// # Ok::<(), velesdb_core::Error>(())
285    /// ```
286    #[must_use]
287    pub fn is_vector(&self) -> bool {
288        matches!(self, Self::Vector(_))
289    }
290
291    /// Returns `true` if this collection is the [`Graph`](Self::Graph) variant.
292    ///
293    /// # Examples
294    ///
295    /// ```rust,no_run
296    /// use velesdb_core::{Database, GraphSchema};
297    ///
298    /// let db = Database::open("./data")?;
299    /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
300    /// let any = db.get_any_collection("edges").expect("exists");
301    /// assert!(any.is_graph());
302    /// # Ok::<(), velesdb_core::Error>(())
303    /// ```
304    #[must_use]
305    pub fn is_graph(&self) -> bool {
306        matches!(self, Self::Graph(_))
307    }
308
309    /// Returns `true` if this collection is the [`Metadata`](Self::Metadata) variant.
310    ///
311    /// # Examples
312    ///
313    /// ```rust,no_run
314    /// use velesdb_core::Database;
315    ///
316    /// let db = Database::open("./data")?;
317    /// db.create_metadata_collection("catalog")?;
318    /// let any = db.get_any_collection("catalog").expect("exists");
319    /// assert!(any.is_metadata());
320    /// # Ok::<(), velesdb_core::Error>(())
321    /// ```
322    #[must_use]
323    pub fn is_metadata(&self) -> bool {
324        matches!(self, Self::Metadata(_))
325    }
326
327    // -------------------------------------------------------------------------
328    // Shared borrows (`as_*`) — zero-cost, return `Option<&T>`
329    // -------------------------------------------------------------------------
330
331    /// Returns a shared reference to the inner [`VectorCollection`] if this is
332    /// the [`Vector`](Self::Vector) variant, or `None` otherwise.
333    ///
334    /// # Examples
335    ///
336    /// ```rust,no_run
337    /// use velesdb_core::{Database, DistanceMetric};
338    ///
339    /// let db = Database::open("./data")?;
340    /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
341    /// let any = db.get_any_collection("docs").expect("exists");
342    /// if let Some(v) = any.as_vector() {
343    ///     let _ = v.config().dimension;
344    /// }
345    /// # Ok::<(), velesdb_core::Error>(())
346    /// ```
347    #[must_use]
348    pub fn as_vector(&self) -> Option<&VectorCollection> {
349        match self {
350            Self::Vector(c) => Some(c),
351            _ => None,
352        }
353    }
354
355    /// Returns an exclusive reference to the inner [`VectorCollection`] if
356    /// this is the [`Vector`](Self::Vector) variant, or `None` otherwise.
357    #[must_use]
358    pub fn as_vector_mut(&mut self) -> Option<&mut VectorCollection> {
359        match self {
360            Self::Vector(c) => Some(c),
361            _ => None,
362        }
363    }
364
365    /// Returns a shared reference to the inner [`GraphCollection`] if this is
366    /// the [`Graph`](Self::Graph) variant, or `None` otherwise.
367    ///
368    /// # Examples
369    ///
370    /// ```rust,no_run
371    /// use velesdb_core::{Database, GraphSchema};
372    ///
373    /// let db = Database::open("./data")?;
374    /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
375    /// let any = db.get_any_collection("edges").expect("exists");
376    /// if let Some(g) = any.as_graph() {
377    ///     let _ = g.edge_count();
378    /// }
379    /// # Ok::<(), velesdb_core::Error>(())
380    /// ```
381    #[must_use]
382    pub fn as_graph(&self) -> Option<&GraphCollection> {
383        match self {
384            Self::Graph(c) => Some(c),
385            _ => None,
386        }
387    }
388
389    /// Returns an exclusive reference to the inner [`GraphCollection`] if
390    /// this is the [`Graph`](Self::Graph) variant, or `None` otherwise.
391    #[must_use]
392    pub fn as_graph_mut(&mut self) -> Option<&mut GraphCollection> {
393        match self {
394            Self::Graph(c) => Some(c),
395            _ => None,
396        }
397    }
398
399    /// Returns a shared reference to the inner [`MetadataCollection`] if this
400    /// is the [`Metadata`](Self::Metadata) variant, or `None` otherwise.
401    ///
402    /// # Examples
403    ///
404    /// ```rust,no_run
405    /// use velesdb_core::Database;
406    ///
407    /// let db = Database::open("./data")?;
408    /// db.create_metadata_collection("catalog")?;
409    /// let any = db.get_any_collection("catalog").expect("exists");
410    /// if let Some(m) = any.as_metadata() {
411    ///     let _ = m.is_empty();
412    /// }
413    /// # Ok::<(), velesdb_core::Error>(())
414    /// ```
415    #[must_use]
416    pub fn as_metadata(&self) -> Option<&MetadataCollection> {
417        match self {
418            Self::Metadata(c) => Some(c),
419            _ => None,
420        }
421    }
422
423    /// Returns an exclusive reference to the inner [`MetadataCollection`] if
424    /// this is the [`Metadata`](Self::Metadata) variant, or `None` otherwise.
425    #[must_use]
426    pub fn as_metadata_mut(&mut self) -> Option<&mut MetadataCollection> {
427        match self {
428            Self::Metadata(c) => Some(c),
429            _ => None,
430        }
431    }
432
433    // -------------------------------------------------------------------------
434    // Consuming conversions (`into_*`) — return `Result<T, Self>` for recovery
435    // -------------------------------------------------------------------------
436
437    /// Consumes `self` and returns the inner [`VectorCollection`] if this is
438    /// the [`Vector`](Self::Vector) variant.
439    ///
440    /// On the wrong variant, returns `Err(self)` so callers can recover
441    /// ownership — mirroring the std [`Result`] / [`TryFrom`] idiom.
442    ///
443    /// # Errors
444    ///
445    /// Returns the original `AnyCollection` unchanged when the variant is
446    /// [`Graph`](Self::Graph) or [`Metadata`](Self::Metadata).
447    ///
448    /// # Examples
449    ///
450    /// ```rust,no_run
451    /// use velesdb_core::{Database, DistanceMetric};
452    ///
453    /// let db = Database::open("./data")?;
454    /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
455    /// let any = db.get_any_collection("docs").expect("exists");
456    /// match any.into_vector() {
457    ///     Ok(v) => { let _ = v.config().dimension; }
458    ///     Err(original) => {
459    ///         // wrong variant; `original` still valid
460    ///         assert!(!original.is_vector());
461    ///     }
462    /// }
463    /// # Ok::<(), velesdb_core::Error>(())
464    /// ```
465    // `Err`-variant is `Self` by design — mirrors std `TryFrom` so callers
466    // recover ownership on the wrong variant. Box-wrapping would defeat the
467    // purpose and forces an allocation on every miss.
468    #[allow(clippy::result_large_err)]
469    pub fn into_vector(self) -> core::result::Result<VectorCollection, Self> {
470        match self {
471            Self::Vector(c) => Ok(c),
472            other => Err(other),
473        }
474    }
475
476    /// Consumes `self` and returns the inner [`GraphCollection`] if this is
477    /// the [`Graph`](Self::Graph) variant.
478    ///
479    /// On the wrong variant, returns `Err(self)` so callers can recover
480    /// ownership.
481    ///
482    /// # Errors
483    ///
484    /// Returns the original `AnyCollection` unchanged when the variant is
485    /// [`Vector`](Self::Vector) or [`Metadata`](Self::Metadata).
486    ///
487    /// # Examples
488    ///
489    /// ```rust,no_run
490    /// use velesdb_core::{Database, GraphSchema};
491    ///
492    /// let db = Database::open("./data")?;
493    /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
494    /// let any = db.get_any_collection("edges").expect("exists");
495    /// match any.into_graph() {
496    ///     Ok(graph) => { let _ = graph.edge_count(); }
497    ///     Err(_wrong_variant) => unreachable!("edges is a graph collection"),
498    /// }
499    /// # Ok::<(), velesdb_core::Error>(())
500    /// ```
501    #[allow(clippy::result_large_err)]
502    pub fn into_graph(self) -> core::result::Result<GraphCollection, Self> {
503        match self {
504            Self::Graph(c) => Ok(c),
505            other => Err(other),
506        }
507    }
508
509    /// Consumes `self` and returns the inner [`MetadataCollection`] if this
510    /// is the [`Metadata`](Self::Metadata) variant.
511    ///
512    /// On the wrong variant, returns `Err(self)` so callers can recover
513    /// ownership.
514    ///
515    /// # Errors
516    ///
517    /// Returns the original `AnyCollection` unchanged when the variant is
518    /// [`Vector`](Self::Vector) or [`Graph`](Self::Graph).
519    ///
520    /// # Examples
521    ///
522    /// ```rust,no_run
523    /// use velesdb_core::Database;
524    ///
525    /// let db = Database::open("./data")?;
526    /// db.create_metadata_collection("catalog")?;
527    /// let any = db.get_any_collection("catalog").expect("exists");
528    /// match any.into_metadata() {
529    ///     Ok(meta) => assert!(meta.is_empty()),
530    ///     Err(_wrong_variant) => unreachable!("catalog is a metadata collection"),
531    /// }
532    /// # Ok::<(), velesdb_core::Error>(())
533    /// ```
534    #[allow(clippy::result_large_err)]
535    pub fn into_metadata(self) -> core::result::Result<MetadataCollection, Self> {
536        match self {
537            Self::Metadata(c) => Ok(c),
538            other => Err(other),
539        }
540    }
541
542    // -------------------------------------------------------------------------
543    // Unchecked cross-cast (escape hatch for SDK bindings)
544    // -------------------------------------------------------------------------
545
546    /// Consumes `self` and returns a [`VectorCollection`] regardless of the
547    /// underlying variant, re-wrapping the shared inner state.
548    ///
549    /// For the [`Vector`](Self::Vector) variant this is a straightforward
550    /// move. For the [`Graph`](Self::Graph) and [`Metadata`](Self::Metadata)
551    /// variants this re-wraps the shared `Arc<Collection>` in the
552    /// `VectorCollection` newtype **without changing the underlying runtime
553    /// type** — downstream code that invokes vector-specific methods on the
554    /// result (for example [`search`](VectorCollection::search),
555    /// [`upsert`](VectorCollection::upsert),
556    /// `config().dimension > 0`) may therefore return empty results or
557    /// misleading state.
558    ///
559    /// This method exists to support the Python / Mobile / Tauri SDK bindings
560    /// that expose a single `Collection` type to users and only invoke the
561    /// shared surface (`config`, `flush`, `diagnostics`, `point_count`,
562    /// `execute_query_str`) on the result.
563    ///
564    /// # Safety
565    ///
566    /// Calling vector-specific methods on a `VectorCollection` obtained from
567    /// a `Graph` or `Metadata` variant is **not** memory-unsafe, but the
568    /// result is logically unsound: the underlying storage does not hold a
569    /// homogeneous vector index, and the returned search results are either
570    /// empty or reflect internal state that was not intended for public
571    /// consumption.
572    ///
573    /// Callers must either:
574    ///
575    /// * branch on [`is_vector`](Self::is_vector) first and only invoke
576    ///   vector-specific methods on the `Vector` variant, or
577    /// * restrict themselves to the methods that all three collection
578    ///   kinds share (`config`, `flush`, `diagnostics`, `name`,
579    ///   `point_count`, `execute_query_str`).
580    ///
581    /// Prefer the safe [`into_vector`](Self::into_vector) (variant-checked,
582    /// returns `Result`) when the caller can branch. A proper type-safe
583    /// refactor that eliminates this method entirely is tracked under the
584    /// post-seed EPIC documented in `docs/ARCHITECTURE.md` (finding F2.2 of
585    /// the pre-seed audit).
586    ///
587    /// # Violation of invariants
588    ///
589    /// The `unsafe` marker flags the caller contract (only invoke the
590    /// shared surface on non-`Vector` variants) even though violating it
591    /// does not cause undefined behaviour.
592    #[must_use]
593    pub unsafe fn into_vector_unchecked(self) -> VectorCollection {
594        match self {
595            Self::Vector(c) => c,
596            Self::Graph(c) => VectorCollection { inner: c.inner },
597            Self::Metadata(c) => VectorCollection { inner: c.inner },
598        }
599    }
600}