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    // Variant discriminants (`is_*`)
176    // -------------------------------------------------------------------------
177
178    /// Returns `true` if this collection is the [`Vector`](Self::Vector) variant.
179    ///
180    /// # Examples
181    ///
182    /// ```rust,no_run
183    /// use velesdb_core::{AnyCollection, Database, DistanceMetric};
184    ///
185    /// let db = Database::open("./data")?;
186    /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
187    /// let any = db.get_any_collection("docs").expect("exists");
188    /// assert!(any.is_vector());
189    /// assert!(!any.is_graph());
190    /// # Ok::<(), velesdb_core::Error>(())
191    /// ```
192    #[must_use]
193    pub fn is_vector(&self) -> bool {
194        matches!(self, Self::Vector(_))
195    }
196
197    /// Returns `true` if this collection is the [`Graph`](Self::Graph) variant.
198    ///
199    /// # Examples
200    ///
201    /// ```rust,no_run
202    /// use velesdb_core::{Database, GraphSchema};
203    ///
204    /// let db = Database::open("./data")?;
205    /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
206    /// let any = db.get_any_collection("edges").expect("exists");
207    /// assert!(any.is_graph());
208    /// # Ok::<(), velesdb_core::Error>(())
209    /// ```
210    #[must_use]
211    pub fn is_graph(&self) -> bool {
212        matches!(self, Self::Graph(_))
213    }
214
215    /// Returns `true` if this collection is the [`Metadata`](Self::Metadata) variant.
216    ///
217    /// # Examples
218    ///
219    /// ```rust,no_run
220    /// use velesdb_core::Database;
221    ///
222    /// let db = Database::open("./data")?;
223    /// db.create_metadata_collection("catalog")?;
224    /// let any = db.get_any_collection("catalog").expect("exists");
225    /// assert!(any.is_metadata());
226    /// # Ok::<(), velesdb_core::Error>(())
227    /// ```
228    #[must_use]
229    pub fn is_metadata(&self) -> bool {
230        matches!(self, Self::Metadata(_))
231    }
232
233    // -------------------------------------------------------------------------
234    // Shared borrows (`as_*`) — zero-cost, return `Option<&T>`
235    // -------------------------------------------------------------------------
236
237    /// Returns a shared reference to the inner [`VectorCollection`] if this is
238    /// the [`Vector`](Self::Vector) variant, or `None` otherwise.
239    ///
240    /// # Examples
241    ///
242    /// ```rust,no_run
243    /// use velesdb_core::{Database, DistanceMetric};
244    ///
245    /// let db = Database::open("./data")?;
246    /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
247    /// let any = db.get_any_collection("docs").expect("exists");
248    /// if let Some(v) = any.as_vector() {
249    ///     let _ = v.config().dimension;
250    /// }
251    /// # Ok::<(), velesdb_core::Error>(())
252    /// ```
253    #[must_use]
254    pub fn as_vector(&self) -> Option<&VectorCollection> {
255        match self {
256            Self::Vector(c) => Some(c),
257            _ => None,
258        }
259    }
260
261    /// Returns an exclusive reference to the inner [`VectorCollection`] if
262    /// this is the [`Vector`](Self::Vector) variant, or `None` otherwise.
263    #[must_use]
264    pub fn as_vector_mut(&mut self) -> Option<&mut VectorCollection> {
265        match self {
266            Self::Vector(c) => Some(c),
267            _ => None,
268        }
269    }
270
271    /// Returns a shared reference to the inner [`GraphCollection`] if this is
272    /// the [`Graph`](Self::Graph) variant, or `None` otherwise.
273    ///
274    /// # Examples
275    ///
276    /// ```rust,no_run
277    /// use velesdb_core::{Database, GraphSchema};
278    ///
279    /// let db = Database::open("./data")?;
280    /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
281    /// let any = db.get_any_collection("edges").expect("exists");
282    /// if let Some(g) = any.as_graph() {
283    ///     let _ = g.edge_count();
284    /// }
285    /// # Ok::<(), velesdb_core::Error>(())
286    /// ```
287    #[must_use]
288    pub fn as_graph(&self) -> Option<&GraphCollection> {
289        match self {
290            Self::Graph(c) => Some(c),
291            _ => None,
292        }
293    }
294
295    /// Returns an exclusive reference to the inner [`GraphCollection`] if
296    /// this is the [`Graph`](Self::Graph) variant, or `None` otherwise.
297    #[must_use]
298    pub fn as_graph_mut(&mut self) -> Option<&mut GraphCollection> {
299        match self {
300            Self::Graph(c) => Some(c),
301            _ => None,
302        }
303    }
304
305    /// Returns a shared reference to the inner [`MetadataCollection`] if this
306    /// is the [`Metadata`](Self::Metadata) variant, or `None` otherwise.
307    ///
308    /// # Examples
309    ///
310    /// ```rust,no_run
311    /// use velesdb_core::Database;
312    ///
313    /// let db = Database::open("./data")?;
314    /// db.create_metadata_collection("catalog")?;
315    /// let any = db.get_any_collection("catalog").expect("exists");
316    /// if let Some(m) = any.as_metadata() {
317    ///     let _ = m.is_empty();
318    /// }
319    /// # Ok::<(), velesdb_core::Error>(())
320    /// ```
321    #[must_use]
322    pub fn as_metadata(&self) -> Option<&MetadataCollection> {
323        match self {
324            Self::Metadata(c) => Some(c),
325            _ => None,
326        }
327    }
328
329    /// Returns an exclusive reference to the inner [`MetadataCollection`] if
330    /// this is the [`Metadata`](Self::Metadata) variant, or `None` otherwise.
331    #[must_use]
332    pub fn as_metadata_mut(&mut self) -> Option<&mut MetadataCollection> {
333        match self {
334            Self::Metadata(c) => Some(c),
335            _ => None,
336        }
337    }
338
339    // -------------------------------------------------------------------------
340    // Consuming conversions (`into_*`) — return `Result<T, Self>` for recovery
341    // -------------------------------------------------------------------------
342
343    /// Consumes `self` and returns the inner [`VectorCollection`] if this is
344    /// the [`Vector`](Self::Vector) variant.
345    ///
346    /// On the wrong variant, returns `Err(self)` so callers can recover
347    /// ownership — mirroring the std [`Result`] / [`TryFrom`] idiom.
348    ///
349    /// # Errors
350    ///
351    /// Returns the original `AnyCollection` unchanged when the variant is
352    /// [`Graph`](Self::Graph) or [`Metadata`](Self::Metadata).
353    ///
354    /// # Examples
355    ///
356    /// ```rust,no_run
357    /// use velesdb_core::{Database, DistanceMetric};
358    ///
359    /// let db = Database::open("./data")?;
360    /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
361    /// let any = db.get_any_collection("docs").expect("exists");
362    /// match any.into_vector() {
363    ///     Ok(v) => { let _ = v.config().dimension; }
364    ///     Err(original) => {
365    ///         // wrong variant; `original` still valid
366    ///         assert!(!original.is_vector());
367    ///     }
368    /// }
369    /// # Ok::<(), velesdb_core::Error>(())
370    /// ```
371    // `Err`-variant is `Self` by design — mirrors std `TryFrom` so callers
372    // recover ownership on the wrong variant. Box-wrapping would defeat the
373    // purpose and forces an allocation on every miss.
374    #[allow(clippy::result_large_err)]
375    pub fn into_vector(self) -> core::result::Result<VectorCollection, Self> {
376        match self {
377            Self::Vector(c) => Ok(c),
378            other => Err(other),
379        }
380    }
381
382    /// Consumes `self` and returns the inner [`GraphCollection`] if this is
383    /// the [`Graph`](Self::Graph) variant.
384    ///
385    /// On the wrong variant, returns `Err(self)` so callers can recover
386    /// ownership.
387    ///
388    /// # Errors
389    ///
390    /// Returns the original `AnyCollection` unchanged when the variant is
391    /// [`Vector`](Self::Vector) or [`Metadata`](Self::Metadata).
392    ///
393    /// # Examples
394    ///
395    /// ```rust,no_run
396    /// use velesdb_core::{Database, GraphSchema};
397    ///
398    /// let db = Database::open("./data")?;
399    /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
400    /// let any = db.get_any_collection("edges").expect("exists");
401    /// match any.into_graph() {
402    ///     Ok(graph) => { let _ = graph.edge_count(); }
403    ///     Err(_wrong_variant) => unreachable!("edges is a graph collection"),
404    /// }
405    /// # Ok::<(), velesdb_core::Error>(())
406    /// ```
407    #[allow(clippy::result_large_err)]
408    pub fn into_graph(self) -> core::result::Result<GraphCollection, Self> {
409        match self {
410            Self::Graph(c) => Ok(c),
411            other => Err(other),
412        }
413    }
414
415    /// Consumes `self` and returns the inner [`MetadataCollection`] if this
416    /// is the [`Metadata`](Self::Metadata) variant.
417    ///
418    /// On the wrong variant, returns `Err(self)` so callers can recover
419    /// ownership.
420    ///
421    /// # Errors
422    ///
423    /// Returns the original `AnyCollection` unchanged when the variant is
424    /// [`Vector`](Self::Vector) or [`Graph`](Self::Graph).
425    ///
426    /// # Examples
427    ///
428    /// ```rust,no_run
429    /// use velesdb_core::Database;
430    ///
431    /// let db = Database::open("./data")?;
432    /// db.create_metadata_collection("catalog")?;
433    /// let any = db.get_any_collection("catalog").expect("exists");
434    /// match any.into_metadata() {
435    ///     Ok(meta) => assert!(meta.is_empty()),
436    ///     Err(_wrong_variant) => unreachable!("catalog is a metadata collection"),
437    /// }
438    /// # Ok::<(), velesdb_core::Error>(())
439    /// ```
440    #[allow(clippy::result_large_err)]
441    pub fn into_metadata(self) -> core::result::Result<MetadataCollection, Self> {
442        match self {
443            Self::Metadata(c) => Ok(c),
444            other => Err(other),
445        }
446    }
447
448    // -------------------------------------------------------------------------
449    // Unchecked cross-cast (escape hatch for SDK bindings)
450    // -------------------------------------------------------------------------
451
452    /// Consumes `self` and returns a [`VectorCollection`] regardless of the
453    /// underlying variant, re-wrapping the shared inner state.
454    ///
455    /// For the [`Vector`](Self::Vector) variant this is a straightforward
456    /// move. For the [`Graph`](Self::Graph) and [`Metadata`](Self::Metadata)
457    /// variants this re-wraps the shared `Arc<Collection>` in the
458    /// `VectorCollection` newtype **without changing the underlying runtime
459    /// type** — downstream code that invokes vector-specific methods on the
460    /// result (for example [`search`](VectorCollection::search),
461    /// [`upsert`](VectorCollection::upsert),
462    /// `config().dimension > 0`) may therefore return empty results or
463    /// misleading state.
464    ///
465    /// This method exists to support the Python / Mobile / Tauri SDK bindings
466    /// that expose a single `Collection` type to users and only invoke the
467    /// shared surface (`config`, `flush`, `diagnostics`, `point_count`,
468    /// `execute_query_str`) on the result.
469    ///
470    /// # Safety
471    ///
472    /// Calling vector-specific methods on a `VectorCollection` obtained from
473    /// a `Graph` or `Metadata` variant is **not** memory-unsafe, but the
474    /// result is logically unsound: the underlying storage does not hold a
475    /// homogeneous vector index, and the returned search results are either
476    /// empty or reflect internal state that was not intended for public
477    /// consumption.
478    ///
479    /// Callers must either:
480    ///
481    /// * branch on [`is_vector`](Self::is_vector) first and only invoke
482    ///   vector-specific methods on the `Vector` variant, or
483    /// * restrict themselves to the methods that all three collection
484    ///   kinds share (`config`, `flush`, `diagnostics`, `name`,
485    ///   `point_count`, `execute_query_str`).
486    ///
487    /// Prefer the safe [`into_vector`](Self::into_vector) (variant-checked,
488    /// returns `Result`) when the caller can branch. A proper type-safe
489    /// refactor that eliminates this method entirely is tracked under the
490    /// post-seed EPIC documented in `docs/ARCHITECTURE.md` (finding F2.2 of
491    /// the pre-seed audit).
492    ///
493    /// # Violation of invariants
494    ///
495    /// The `unsafe` marker flags the caller contract (only invoke the
496    /// shared surface on non-`Vector` variants) even though violating it
497    /// does not cause undefined behaviour.
498    #[must_use]
499    pub unsafe fn into_vector_unchecked(self) -> VectorCollection {
500        match self {
501            Self::Vector(c) => c,
502            Self::Graph(c) => VectorCollection { inner: c.inner },
503            Self::Metadata(c) => VectorCollection { inner: c.inner },
504        }
505    }
506}