manifoldb_graph/store/node.rs
1//! Node (entity) storage operations.
2//!
3//! This module provides CRUD operations for nodes in the graph.
4
5use std::ops::Bound;
6
7use manifoldb_core::encoding::keys::{
8 decode_entity_key, decode_label_index_entity_id, encode_entity_key, encode_label_index_key,
9 encode_label_index_prefix, PREFIX_ENTITY,
10};
11use manifoldb_core::encoding::{Decoder, Encoder};
12use manifoldb_core::{Entity, EntityId, Label};
13use manifoldb_storage::{Cursor, Transaction};
14
15use super::error::{GraphError, GraphResult};
16use super::IdGenerator;
17
18/// Table name for entity data.
19pub const TABLE_ENTITIES: &str = "entities";
20
21/// Table name for label index.
22pub const TABLE_LABELS: &str = "labels";
23
24/// Node storage operations.
25///
26/// `NodeStore` provides transactional CRUD operations for graph nodes (entities).
27/// All operations work within a transaction context for ACID guarantees.
28///
29/// # Example
30///
31/// ```ignore
32/// use manifoldb_graph::store::{NodeStore, IdGenerator};
33///
34/// // Create a node
35/// let gen = IdGenerator::new();
36/// let entity = NodeStore::create(&mut tx, &gen, |id| {
37/// Entity::new(id)
38/// .with_label("Person")
39/// .with_property("name", "Alice")
40/// })?;
41///
42/// // Read it back
43/// let retrieved = NodeStore::get(&tx, entity.id)?;
44/// ```
45pub struct NodeStore;
46
47impl NodeStore {
48 /// Create a new entity in the store.
49 ///
50 /// The provided function receives a new unique ID and should return
51 /// the entity to store. The entity's ID will be set to the generated ID.
52 ///
53 /// # Arguments
54 ///
55 /// * `tx` - The transaction to use
56 /// * `id_gen` - The ID generator
57 /// * `builder` - A function that builds the entity given an ID
58 ///
59 /// # Returns
60 ///
61 /// The created entity with its assigned ID.
62 ///
63 /// # Errors
64 ///
65 /// Returns an error if the entity cannot be stored.
66 pub fn create<T: Transaction, F>(
67 tx: &mut T,
68 id_gen: &IdGenerator,
69 builder: F,
70 ) -> GraphResult<Entity>
71 where
72 F: FnOnce(EntityId) -> Entity,
73 {
74 let id = id_gen.next_entity_id();
75 let entity = builder(id);
76
77 // Encode and store the entity
78 let key = encode_entity_key(id);
79 let value = entity.encode()?;
80 tx.put(TABLE_ENTITIES, &key, &value)?;
81
82 // Index all labels
83 for label in &entity.labels {
84 let label_key = encode_label_index_key(label, id);
85 tx.put(TABLE_LABELS, &label_key, &[])?;
86 }
87
88 Ok(entity)
89 }
90
91 /// Create an entity with a specific ID.
92 ///
93 /// This is useful when importing data or when you need to control IDs.
94 ///
95 /// # Arguments
96 ///
97 /// * `tx` - The transaction to use
98 /// * `entity` - The entity to store (must have a valid ID)
99 ///
100 /// # Errors
101 ///
102 /// Returns [`GraphError::EntityAlreadyExists`] if an entity with this ID exists.
103 pub fn create_with_id<T: Transaction>(tx: &mut T, entity: &Entity) -> GraphResult<()> {
104 let key = encode_entity_key(entity.id);
105
106 // Check if entity already exists
107 if tx.get(TABLE_ENTITIES, &key)?.is_some() {
108 return Err(GraphError::EntityAlreadyExists(entity.id));
109 }
110
111 // Store the entity
112 let value = entity.encode()?;
113 tx.put(TABLE_ENTITIES, &key, &value)?;
114
115 // Index all labels
116 for label in &entity.labels {
117 let label_key = encode_label_index_key(label, entity.id);
118 tx.put(TABLE_LABELS, &label_key, &[])?;
119 }
120
121 Ok(())
122 }
123
124 /// Get an entity by ID.
125 ///
126 /// # Arguments
127 ///
128 /// * `tx` - The transaction to use
129 /// * `id` - The entity ID to look up
130 ///
131 /// # Returns
132 ///
133 /// The entity if found, or `None` if it doesn't exist.
134 ///
135 /// # Errors
136 ///
137 /// Returns an error if the entity cannot be decoded.
138 pub fn get<T: Transaction>(tx: &T, id: EntityId) -> GraphResult<Option<Entity>> {
139 let key = encode_entity_key(id);
140 match tx.get(TABLE_ENTITIES, &key)? {
141 Some(value) => {
142 let entity = Entity::decode(&value)?;
143 Ok(Some(entity))
144 }
145 None => Ok(None),
146 }
147 }
148
149 /// Get an entity by ID, returning an error if not found.
150 ///
151 /// # Arguments
152 ///
153 /// * `tx` - The transaction to use
154 /// * `id` - The entity ID to look up
155 ///
156 /// # Errors
157 ///
158 /// Returns [`GraphError::EntityNotFound`] if the entity doesn't exist.
159 pub fn get_or_error<T: Transaction>(tx: &T, id: EntityId) -> GraphResult<Entity> {
160 Self::get(tx, id)?.ok_or(GraphError::EntityNotFound(id))
161 }
162
163 /// Check if an entity exists.
164 ///
165 /// # Arguments
166 ///
167 /// * `tx` - The transaction to use
168 /// * `id` - The entity ID to check
169 pub fn exists<T: Transaction>(tx: &T, id: EntityId) -> GraphResult<bool> {
170 let key = encode_entity_key(id);
171 Ok(tx.get(TABLE_ENTITIES, &key)?.is_some())
172 }
173
174 /// Update an existing entity.
175 ///
176 /// This replaces the entire entity. To update specific fields,
177 /// first get the entity, modify it, then update.
178 ///
179 /// # Arguments
180 ///
181 /// * `tx` - The transaction to use
182 /// * `entity` - The entity with updated data
183 ///
184 /// # Errors
185 ///
186 /// Returns [`GraphError::EntityNotFound`] if the entity doesn't exist.
187 pub fn update<T: Transaction>(tx: &mut T, entity: &Entity) -> GraphResult<()> {
188 let key = encode_entity_key(entity.id);
189
190 // Get the old entity to update label indexes
191 let old_value =
192 tx.get(TABLE_ENTITIES, &key)?.ok_or(GraphError::EntityNotFound(entity.id))?;
193 let old_entity = Entity::decode(&old_value)?;
194
195 // Remove old label indexes
196 for label in &old_entity.labels {
197 let label_key = encode_label_index_key(label, entity.id);
198 tx.delete(TABLE_LABELS, &label_key)?;
199 }
200
201 // Store updated entity
202 let value = entity.encode()?;
203 tx.put(TABLE_ENTITIES, &key, &value)?;
204
205 // Add new label indexes
206 for label in &entity.labels {
207 let label_key = encode_label_index_key(label, entity.id);
208 tx.put(TABLE_LABELS, &label_key, &[])?;
209 }
210
211 Ok(())
212 }
213
214 /// Delete an entity by ID.
215 ///
216 /// This also removes all label index entries for the entity.
217 /// Note: This does NOT delete edges connected to this entity.
218 /// Use [`crate::store::EdgeStore::delete_edges_for_entity`] to clean up edges first.
219 ///
220 /// # Arguments
221 ///
222 /// * `tx` - The transaction to use
223 /// * `id` - The entity ID to delete
224 ///
225 /// # Returns
226 ///
227 /// `true` if the entity was deleted, `false` if it didn't exist.
228 pub fn delete<T: Transaction>(tx: &mut T, id: EntityId) -> GraphResult<bool> {
229 let key = encode_entity_key(id);
230
231 // Get the entity to clean up label indexes
232 let Some(value) = tx.get(TABLE_ENTITIES, &key)? else {
233 return Ok(false);
234 };
235 let entity = Entity::decode(&value)?;
236
237 // Remove label indexes
238 for label in &entity.labels {
239 let label_key = encode_label_index_key(label, id);
240 tx.delete(TABLE_LABELS, &label_key)?;
241 }
242
243 // Delete the entity
244 tx.delete(TABLE_ENTITIES, &key)?;
245 Ok(true)
246 }
247
248 /// Find all entities with a specific label.
249 ///
250 /// # Arguments
251 ///
252 /// * `tx` - The transaction to use
253 /// * `label` - The label to search for
254 ///
255 /// # Returns
256 ///
257 /// A vector of entity IDs that have the label.
258 pub fn find_by_label<T: Transaction>(tx: &T, label: &Label) -> GraphResult<Vec<EntityId>> {
259 let prefix = encode_label_index_prefix(label);
260
261 // Create the end bound by incrementing the last byte of the prefix
262 let mut end_prefix = prefix.clone();
263 if let Some(last) = end_prefix.last_mut() {
264 *last = last.saturating_add(1);
265 }
266
267 let mut cursor = tx.range(
268 TABLE_LABELS,
269 Bound::Included(prefix.as_slice()),
270 Bound::Excluded(end_prefix.as_slice()),
271 )?;
272
273 let mut ids = Vec::new();
274 while let Some((key, _)) = cursor.next()? {
275 if let Some(id) = decode_label_index_entity_id(&key) {
276 ids.push(id);
277 }
278 }
279
280 Ok(ids)
281 }
282
283 /// Count all entities in the store.
284 ///
285 /// # Arguments
286 ///
287 /// * `tx` - The transaction to use
288 pub fn count<T: Transaction>(tx: &T) -> GraphResult<usize> {
289 let start = [PREFIX_ENTITY];
290 let end = [PREFIX_ENTITY + 1];
291
292 let cursor_result = tx.range(
293 TABLE_ENTITIES,
294 Bound::Included(start.as_slice()),
295 Bound::Excluded(end.as_slice()),
296 );
297
298 // Handle table not existing (empty store)
299 let mut cursor = match cursor_result {
300 Ok(c) => c,
301 Err(manifoldb_storage::StorageError::TableNotFound(_)) => return Ok(0),
302 Err(e) => return Err(e.into()),
303 };
304
305 let mut count = 0;
306 while cursor.next()?.is_some() {
307 count += 1;
308 }
309
310 Ok(count)
311 }
312
313 /// Iterate over all entities.
314 ///
315 /// # Arguments
316 ///
317 /// * `tx` - The transaction to use
318 /// * `f` - A function to call for each entity. Return `false` to stop iteration.
319 ///
320 /// # Errors
321 ///
322 /// Returns an error if iteration fails or if any entity cannot be decoded.
323 pub fn for_each<T: Transaction, F>(tx: &T, mut f: F) -> GraphResult<()>
324 where
325 F: FnMut(&Entity) -> bool,
326 {
327 let start = [PREFIX_ENTITY];
328 let end = [PREFIX_ENTITY + 1];
329
330 let cursor_result = tx.range(
331 TABLE_ENTITIES,
332 Bound::Included(start.as_slice()),
333 Bound::Excluded(end.as_slice()),
334 );
335
336 // Handle table not existing (empty store)
337 let mut cursor = match cursor_result {
338 Ok(c) => c,
339 Err(manifoldb_storage::StorageError::TableNotFound(_)) => return Ok(()),
340 Err(e) => return Err(e.into()),
341 };
342
343 while let Some((_, value)) = cursor.next()? {
344 let entity = Entity::decode(&value)?;
345 if !f(&entity) {
346 break;
347 }
348 }
349
350 Ok(())
351 }
352
353 /// Get all entities as a vector.
354 ///
355 /// Use with caution on large datasets - prefer [`Self::for_each`] for
356 /// processing entities without loading all into memory.
357 ///
358 /// # Arguments
359 ///
360 /// * `tx` - The transaction to use
361 pub fn all<T: Transaction>(tx: &T) -> GraphResult<Vec<Entity>> {
362 let mut entities = Vec::new();
363 Self::for_each(tx, |entity| {
364 entities.push(entity.clone());
365 true
366 })?;
367 Ok(entities)
368 }
369
370 /// Find the highest entity ID in the store.
371 ///
372 /// This is useful for initializing the ID generator after loading data.
373 ///
374 /// # Arguments
375 ///
376 /// * `tx` - The transaction to use
377 ///
378 /// # Returns
379 ///
380 /// The highest entity ID, or `None` if there are no entities.
381 pub fn max_id<T: Transaction>(tx: &T) -> GraphResult<Option<EntityId>> {
382 let start = [PREFIX_ENTITY];
383 let end = [PREFIX_ENTITY + 1];
384
385 let cursor_result = tx.range(
386 TABLE_ENTITIES,
387 Bound::Included(start.as_slice()),
388 Bound::Excluded(end.as_slice()),
389 );
390
391 // Handle table not existing (empty store)
392 let mut cursor = match cursor_result {
393 Ok(c) => c,
394 Err(manifoldb_storage::StorageError::TableNotFound(_)) => return Ok(None),
395 Err(e) => return Err(e.into()),
396 };
397
398 // Seek to the last key in the range
399 if cursor.seek_last()?.is_some() {
400 if let Some((key, _)) = cursor.current().map(|(k, v)| (k.to_vec(), v.to_vec())) {
401 return Ok(decode_entity_key(&key));
402 }
403 }
404
405 Ok(None)
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 // Note: Integration tests with actual storage backend are in the tests/ directory
414
415 #[test]
416 fn table_names_are_valid() {
417 assert!(!TABLE_ENTITIES.is_empty());
418 assert!(!TABLE_LABELS.is_empty());
419 assert_ne!(TABLE_ENTITIES, TABLE_LABELS);
420 }
421}