Skip to main content

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}