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}