velesdb_core/collection/metadata_collection.rs
1//! `MetadataCollection`: payload-only storage without vectors.
2//!
3//! Ideal for reference tables, catalogs, and structured metadata.
4//! Supports CRUD and VelesQL queries on payload — NOT vector search.
5//!
6//! # Design
7//!
8//! `MetadataCollection` is a pure newtype over `Collection` — all operations
9//! delegate to the single `inner` instance, matching the `VectorCollection` pattern
10//! and eliminating any dual-storage desync risk (C-02).
11
12use std::collections::HashMap;
13use std::path::PathBuf;
14
15use crate::collection::types::Collection;
16use crate::error::{Error, Result};
17use crate::point::{Point, SearchResult};
18
19/// A metadata-only collection storing structured payloads without vector indexes.
20///
21/// # Examples
22///
23/// ```rust,no_run
24/// use velesdb_core::{MetadataCollection, Point};
25/// use serde_json::json;
26///
27/// let coll = MetadataCollection::create("./data/products".into(), "products")?;
28///
29/// coll.upsert(vec![
30/// Point::metadata_only(1, json!({"name": "Widget", "price": 9.99})),
31/// ])?;
32/// # Ok::<(), velesdb_core::Error>(())
33/// ```
34#[derive(Clone)]
35pub struct MetadataCollection {
36 /// Single source of truth — all operations delegate here (C-02 pure newtype).
37 pub(crate) inner: Collection,
38}
39
40impl MetadataCollection {
41 // -------------------------------------------------------------------------
42 // Lifecycle
43 // -------------------------------------------------------------------------
44
45 /// Creates a new `MetadataCollection`.
46 ///
47 /// # Errors
48 ///
49 /// Returns an error if the directory cannot be created or storage fails.
50 pub fn create(path: PathBuf, name: &str) -> Result<Self> {
51 Ok(Self {
52 inner: Collection::create_metadata_only(path, name)?,
53 })
54 }
55
56 /// Opens an existing `MetadataCollection` from disk.
57 ///
58 /// # Errors
59 ///
60 /// Returns an error if config or storage cannot be opened.
61 pub fn open(path: PathBuf) -> Result<Self> {
62 Ok(Self {
63 inner: Collection::open(path)?,
64 })
65 }
66
67 /// Consumes `self` and returns a [`VectorCollection`](super::VectorCollection)
68 /// **structural view** over this metadata collection's shared `inner` store.
69 ///
70 /// Exact mirror of
71 /// [`GraphCollection::into_vector_view`](super::GraphCollection::into_vector_view)
72 /// — see that method for the full contract (purely structural re-wrap, no
73 /// vector-kind assertion, Python-binding-only rationale).
74 #[must_use]
75 pub fn into_vector_view(self) -> super::VectorCollection {
76 super::VectorCollection { inner: self.inner }
77 }
78
79 /// Flushes to disk.
80 ///
81 /// Issue #423: This fast-path flush skips `vectors.idx` serialization.
82 /// The WAL provides crash recovery for the vector index.
83 ///
84 /// # Errors
85 ///
86 /// Returns an error if the flush fails.
87 pub fn flush(&self) -> Result<()> {
88 self.inner.flush()
89 }
90
91 /// Full durability flush including `vectors.idx` serialization.
92 ///
93 /// Issue #423: Use on graceful shutdown to avoid a full WAL replay
94 /// on the next startup.
95 ///
96 /// # Errors
97 ///
98 /// Returns an error if the flush fails.
99 pub fn flush_full(&self) -> Result<()> {
100 self.inner.flush_full()
101 }
102
103 // -------------------------------------------------------------------------
104 // Metadata
105 // -------------------------------------------------------------------------
106
107 /// Returns the collection name.
108 #[must_use]
109 pub fn name(&self) -> String {
110 self.inner.config().name
111 }
112
113 /// Returns the number of items in the collection.
114 #[must_use]
115 pub fn len(&self) -> usize {
116 self.inner.len()
117 }
118
119 /// Returns `true` if the collection is empty.
120 #[must_use]
121 pub fn is_empty(&self) -> bool {
122 self.inner.is_empty()
123 }
124
125 /// Returns the collection configuration.
126 #[must_use]
127 pub fn config(&self) -> crate::collection::CollectionConfig {
128 self.inner.config()
129 }
130
131 /// Returns `true` — metadata collections are always metadata-only.
132 #[must_use]
133 pub fn is_metadata_only(&self) -> bool {
134 true
135 }
136
137 /// Inserts or updates metadata-only points (convenience alias for `upsert`).
138 ///
139 /// # Errors
140 ///
141 /// Returns an error if a point carries a non-empty vector.
142 pub fn upsert_metadata(&self, points: impl IntoIterator<Item = Point>) -> Result<()> {
143 self.upsert(points)
144 }
145
146 /// Returns all stored IDs.
147 #[must_use]
148 pub fn all_ids(&self) -> Vec<u64> {
149 self.inner.all_ids()
150 }
151
152 /// Returns the next batch of points for scroll iteration.
153 ///
154 /// Delegates to the inner collection's `scroll_batch` (parallel
155 /// implementation to [`VectorCollection::scroll_batch`](crate::VectorCollection::scroll_batch)).
156 ///
157 /// # Errors
158 ///
159 /// Returns an error if `batch_size` is 0.
160 pub fn scroll_batch(
161 &self,
162 cursor: Option<u64>,
163 batch_size: usize,
164 filter: Option<&crate::filter::Filter>,
165 ) -> Result<crate::collection::ScrollBatch> {
166 self.inner.scroll_batch(cursor, batch_size, filter)
167 }
168
169 // -------------------------------------------------------------------------
170 // CRUD
171 // -------------------------------------------------------------------------
172
173 /// Inserts or updates metadata points (must have no vector).
174 ///
175 /// # Errors
176 ///
177 /// Returns an error if a point carries a non-empty vector,
178 /// or if storage operations fail.
179 pub fn upsert(&self, points: impl IntoIterator<Item = Point>) -> Result<()> {
180 let points: Vec<Point> = points.into_iter().collect();
181 let name = self.inner.config().name;
182
183 for point in &points {
184 if !point.vector.is_empty() {
185 return Err(Error::VectorNotAllowed(name.clone()));
186 }
187 }
188
189 self.inner.upsert_metadata(points)
190 }
191
192 /// Retrieves items by IDs.
193 #[must_use]
194 pub fn get(&self, ids: &[u64]) -> Vec<Option<Point>> {
195 self.inner.get(ids)
196 }
197
198 /// Deletes items by IDs.
199 ///
200 /// # Errors
201 ///
202 /// Returns an error if storage operations fail.
203 pub fn delete(&self, ids: &[u64]) -> Result<()> {
204 self.inner.delete(ids)
205 }
206
207 // -------------------------------------------------------------------------
208 // Text search
209 // -------------------------------------------------------------------------
210
211 /// Performs BM25 full-text search over payloads.
212 ///
213 /// # Errors
214 ///
215 /// Returns an error if storage retrieval fails.
216 pub fn text_search(&self, query: &str, k: usize) -> Result<Vec<SearchResult>> {
217 self.inner.text_search(query, k)
218 }
219
220 /// Performs vector similarity search.
221 ///
222 /// Note: metadata-only collections have no vectors, so this will
223 /// return an empty result set.
224 ///
225 /// # Errors
226 ///
227 /// Returns an error if the search fails.
228 pub fn search(&self, query: &[f32], k: usize) -> Result<Vec<SearchResult>> {
229 self.inner.search(query, k)
230 }
231
232 // -------------------------------------------------------------------------
233 // VelesQL
234 // -------------------------------------------------------------------------
235
236 /// Executes a `VelesQL` query.
237 ///
238 /// # Errors
239 ///
240 /// Returns an error if the query is invalid or execution fails.
241 pub fn execute_query(
242 &self,
243 query: &crate::velesql::Query,
244 params: &HashMap<String, serde_json::Value>,
245 ) -> Result<Vec<SearchResult>> {
246 self.inner.execute_query(query, params)
247 }
248
249 /// Executes a raw VelesQL string.
250 ///
251 /// # Errors
252 ///
253 /// Returns an error if parsing or execution fails.
254 pub fn execute_query_str(
255 &self,
256 sql: &str,
257 params: &HashMap<String, serde_json::Value>,
258 ) -> Result<Vec<SearchResult>> {
259 self.inner.execute_query_str(sql, params)
260 }
261}