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 // One barrier for the batch, matching the Vector and Metadata
240 // arms. This used to loop the single-node path, paying an
241 // fsync, an auto-snapshot check and two label-index
242 // acquisitions per node (#2153).
243 let entries: Vec<(u64, &serde_json::Value)> = points
244 .iter()
245 .filter_map(|p| p.payload.as_ref().map(|payload| (p.id, payload)))
246 .collect();
247 c.upsert_node_payloads(&entries)
248 }
249 Self::Metadata(c) => c.upsert(points),
250 }
251 }
252
253 // -------------------------------------------------------------------------
254 // Variant discriminants (`is_*`)
255 // -------------------------------------------------------------------------
256
257 /// Returns `true` if this collection is the [`Vector`](Self::Vector) variant.
258 ///
259 /// # Examples
260 ///
261 /// ```rust,no_run
262 /// use velesdb_core::{AnyCollection, Database, DistanceMetric};
263 ///
264 /// let db = Database::open("./data")?;
265 /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
266 /// let any = db.get_any_collection("docs").expect("exists");
267 /// assert!(any.is_vector());
268 /// assert!(!any.is_graph());
269 /// # Ok::<(), velesdb_core::Error>(())
270 /// ```
271 #[must_use]
272 pub fn is_vector(&self) -> bool {
273 matches!(self, Self::Vector(_))
274 }
275
276 /// Returns `true` if this collection is the [`Graph`](Self::Graph) variant.
277 ///
278 /// # Examples
279 ///
280 /// ```rust,no_run
281 /// use velesdb_core::{Database, GraphSchema};
282 ///
283 /// let db = Database::open("./data")?;
284 /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
285 /// let any = db.get_any_collection("edges").expect("exists");
286 /// assert!(any.is_graph());
287 /// # Ok::<(), velesdb_core::Error>(())
288 /// ```
289 #[must_use]
290 pub fn is_graph(&self) -> bool {
291 matches!(self, Self::Graph(_))
292 }
293
294 /// Returns `true` if this collection is the [`Metadata`](Self::Metadata) variant.
295 ///
296 /// # Examples
297 ///
298 /// ```rust,no_run
299 /// use velesdb_core::Database;
300 ///
301 /// let db = Database::open("./data")?;
302 /// db.create_metadata_collection("catalog")?;
303 /// let any = db.get_any_collection("catalog").expect("exists");
304 /// assert!(any.is_metadata());
305 /// # Ok::<(), velesdb_core::Error>(())
306 /// ```
307 #[must_use]
308 pub fn is_metadata(&self) -> bool {
309 matches!(self, Self::Metadata(_))
310 }
311
312 // -------------------------------------------------------------------------
313 // Shared borrows (`as_*`) — zero-cost, return `Option<&T>`
314 // -------------------------------------------------------------------------
315
316 /// Returns a shared reference to the inner [`VectorCollection`] if this is
317 /// the [`Vector`](Self::Vector) variant, or `None` otherwise.
318 ///
319 /// # Examples
320 ///
321 /// ```rust,no_run
322 /// use velesdb_core::{Database, DistanceMetric};
323 ///
324 /// let db = Database::open("./data")?;
325 /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
326 /// let any = db.get_any_collection("docs").expect("exists");
327 /// if let Some(v) = any.as_vector() {
328 /// let _ = v.config().dimension;
329 /// }
330 /// # Ok::<(), velesdb_core::Error>(())
331 /// ```
332 #[must_use]
333 pub fn as_vector(&self) -> Option<&VectorCollection> {
334 match self {
335 Self::Vector(c) => Some(c),
336 _ => None,
337 }
338 }
339
340 /// Returns an exclusive reference to the inner [`VectorCollection`] if
341 /// this is the [`Vector`](Self::Vector) variant, or `None` otherwise.
342 #[must_use]
343 pub fn as_vector_mut(&mut self) -> Option<&mut VectorCollection> {
344 match self {
345 Self::Vector(c) => Some(c),
346 _ => None,
347 }
348 }
349
350 /// Returns a shared reference to the inner [`GraphCollection`] if this is
351 /// the [`Graph`](Self::Graph) variant, or `None` otherwise.
352 ///
353 /// # Examples
354 ///
355 /// ```rust,no_run
356 /// use velesdb_core::{Database, GraphSchema};
357 ///
358 /// let db = Database::open("./data")?;
359 /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
360 /// let any = db.get_any_collection("edges").expect("exists");
361 /// if let Some(g) = any.as_graph() {
362 /// let _ = g.edge_count();
363 /// }
364 /// # Ok::<(), velesdb_core::Error>(())
365 /// ```
366 #[must_use]
367 pub fn as_graph(&self) -> Option<&GraphCollection> {
368 match self {
369 Self::Graph(c) => Some(c),
370 _ => None,
371 }
372 }
373
374 /// Returns an exclusive reference to the inner [`GraphCollection`] if
375 /// this is the [`Graph`](Self::Graph) variant, or `None` otherwise.
376 #[must_use]
377 pub fn as_graph_mut(&mut self) -> Option<&mut GraphCollection> {
378 match self {
379 Self::Graph(c) => Some(c),
380 _ => None,
381 }
382 }
383
384 /// Returns a shared reference to the inner [`MetadataCollection`] if this
385 /// is the [`Metadata`](Self::Metadata) variant, or `None` otherwise.
386 ///
387 /// # Examples
388 ///
389 /// ```rust,no_run
390 /// use velesdb_core::Database;
391 ///
392 /// let db = Database::open("./data")?;
393 /// db.create_metadata_collection("catalog")?;
394 /// let any = db.get_any_collection("catalog").expect("exists");
395 /// if let Some(m) = any.as_metadata() {
396 /// let _ = m.is_empty();
397 /// }
398 /// # Ok::<(), velesdb_core::Error>(())
399 /// ```
400 #[must_use]
401 pub fn as_metadata(&self) -> Option<&MetadataCollection> {
402 match self {
403 Self::Metadata(c) => Some(c),
404 _ => None,
405 }
406 }
407
408 /// Returns an exclusive reference to the inner [`MetadataCollection`] if
409 /// this is the [`Metadata`](Self::Metadata) variant, or `None` otherwise.
410 #[must_use]
411 pub fn as_metadata_mut(&mut self) -> Option<&mut MetadataCollection> {
412 match self {
413 Self::Metadata(c) => Some(c),
414 _ => None,
415 }
416 }
417
418 // -------------------------------------------------------------------------
419 // Consuming conversions (`into_*`) — return `Result<T, Self>` for recovery
420 // -------------------------------------------------------------------------
421
422 /// Consumes `self` and returns the inner [`VectorCollection`] if this is
423 /// the [`Vector`](Self::Vector) variant.
424 ///
425 /// On the wrong variant, returns `Err(self)` so callers can recover
426 /// ownership — mirroring the std [`Result`] / [`TryFrom`] idiom.
427 ///
428 /// # Errors
429 ///
430 /// Returns the original `AnyCollection` unchanged when the variant is
431 /// [`Graph`](Self::Graph) or [`Metadata`](Self::Metadata).
432 ///
433 /// # Examples
434 ///
435 /// ```rust,no_run
436 /// use velesdb_core::{Database, DistanceMetric};
437 ///
438 /// let db = Database::open("./data")?;
439 /// db.create_collection("docs", 768, DistanceMetric::Cosine)?;
440 /// let any = db.get_any_collection("docs").expect("exists");
441 /// match any.into_vector() {
442 /// Ok(v) => { let _ = v.config().dimension; }
443 /// Err(original) => {
444 /// // wrong variant; `original` still valid
445 /// assert!(!original.is_vector());
446 /// }
447 /// }
448 /// # Ok::<(), velesdb_core::Error>(())
449 /// ```
450 // `Err`-variant is `Self` by design — mirrors std `TryFrom` so callers
451 // recover ownership on the wrong variant. Box-wrapping would defeat the
452 // purpose and forces an allocation on every miss.
453 #[allow(clippy::result_large_err)]
454 pub fn into_vector(self) -> core::result::Result<VectorCollection, Self> {
455 match self {
456 Self::Vector(c) => Ok(c),
457 other => Err(other),
458 }
459 }
460
461 /// Consumes `self` and returns the inner [`GraphCollection`] if this is
462 /// the [`Graph`](Self::Graph) variant.
463 ///
464 /// On the wrong variant, returns `Err(self)` so callers can recover
465 /// ownership.
466 ///
467 /// # Errors
468 ///
469 /// Returns the original `AnyCollection` unchanged when the variant is
470 /// [`Vector`](Self::Vector) or [`Metadata`](Self::Metadata).
471 ///
472 /// # Examples
473 ///
474 /// ```rust,no_run
475 /// use velesdb_core::{Database, GraphSchema};
476 ///
477 /// let db = Database::open("./data")?;
478 /// db.create_graph_collection("edges", GraphSchema::schemaless())?;
479 /// let any = db.get_any_collection("edges").expect("exists");
480 /// match any.into_graph() {
481 /// Ok(graph) => { let _ = graph.edge_count(); }
482 /// Err(_wrong_variant) => unreachable!("edges is a graph collection"),
483 /// }
484 /// # Ok::<(), velesdb_core::Error>(())
485 /// ```
486 #[allow(clippy::result_large_err)]
487 pub fn into_graph(self) -> core::result::Result<GraphCollection, Self> {
488 match self {
489 Self::Graph(c) => Ok(c),
490 other => Err(other),
491 }
492 }
493
494 /// Consumes `self` and returns the inner [`MetadataCollection`] if this
495 /// is the [`Metadata`](Self::Metadata) variant.
496 ///
497 /// On the wrong variant, returns `Err(self)` so callers can recover
498 /// ownership.
499 ///
500 /// # Errors
501 ///
502 /// Returns the original `AnyCollection` unchanged when the variant is
503 /// [`Vector`](Self::Vector) or [`Graph`](Self::Graph).
504 ///
505 /// # Examples
506 ///
507 /// ```rust,no_run
508 /// use velesdb_core::Database;
509 ///
510 /// let db = Database::open("./data")?;
511 /// db.create_metadata_collection("catalog")?;
512 /// let any = db.get_any_collection("catalog").expect("exists");
513 /// match any.into_metadata() {
514 /// Ok(meta) => assert!(meta.is_empty()),
515 /// Err(_wrong_variant) => unreachable!("catalog is a metadata collection"),
516 /// }
517 /// # Ok::<(), velesdb_core::Error>(())
518 /// ```
519 #[allow(clippy::result_large_err)]
520 pub fn into_metadata(self) -> core::result::Result<MetadataCollection, Self> {
521 match self {
522 Self::Metadata(c) => Ok(c),
523 other => Err(other),
524 }
525 }
526}