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}