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}