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