ggen_core/graph/
store.rs

1//! GraphStore - Persistent storage operations
2//!
3//! Provides operations for creating and managing persistent on-disk RDF stores
4//! using Oxigraph's RocksDB backend.
5
6use crate::graph::core::Graph;
7use ggen_utils::error::Result;
8use oxigraph::store::Store;
9use std::path::Path;
10use std::sync::Arc;
11
12/// GraphStore provides persistent storage operations for RDF graphs.
13///
14/// This type wraps Oxigraph's persistent storage capabilities, allowing
15/// graphs to be stored on disk using RocksDB.
16///
17/// # Examples
18///
19/// ```rust,no_run
20/// use ggen_core::graph::{Graph, GraphStore};
21///
22/// # fn main() -> ggen_utils::error::Result<()> {
23/// // Open or create a persistent store
24/// let store = GraphStore::open("./data/store")?;
25///
26/// // Create a graph from the persistent store
27/// let graph = store.create_graph()?;
28///
29/// // Use the graph normally
30/// graph.insert_turtle(r#"
31///     @prefix ex: <http://example.org/> .
32///     ex:alice a ex:Person .
33/// "#)?;
34///
35/// // Data is persisted to disk
36/// # Ok(())
37/// # }
38/// ```
39pub struct GraphStore {
40    store: Arc<Store>,
41}
42
43impl GraphStore {
44    /// Open or create a persistent RDF store at the given path.
45    ///
46    /// The store uses RocksDB for persistence. If the path doesn't exist,
47    /// a new store will be created.
48    ///
49    /// # Arguments
50    ///
51    /// * `path` - Path to the store directory
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if:
56    /// - The path cannot be created or accessed
57    /// - The store is corrupted
58    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
59        // **Root Cause Fix**: Use explicit `.map_err()` for oxigraph error conversion
60        // Pattern: Always use `.map_err()` for external library errors that don't implement `From`
61
62        // Ensure parent directory exists (Oxigraph doesn't create it automatically)
63        let path_ref = path.as_ref();
64        if let Some(parent) = path_ref.parent() {
65            std::fs::create_dir_all(parent).map_err(|e| {
66                ggen_utils::error::Error::new(&format!(
67                    "Failed to create store parent directory: {}",
68                    e
69                ))
70            })?;
71        }
72
73        let store = Store::open(path_ref)
74            .map_err(|e| ggen_utils::error::Error::new(&format!("Failed to open store: {}", e)))?;
75        Ok(Self {
76            store: Arc::new(store),
77        })
78    }
79
80    /// Create a new in-memory store.
81    ///
82    /// Creates a new in-memory RDF store. This is similar to `Graph::new()` but
83    /// returns a `GraphStore` instead of a `Graph`, providing a consistent API
84    /// for store management.
85    pub fn new() -> Result<Self> {
86        // **Root Cause Fix**: Use explicit `.map_err()` for oxigraph error conversion
87        // Pattern: Always use `.map_err()` for external library errors that don't implement `From`
88        let store = Store::new().map_err(|e| {
89            ggen_utils::error::Error::new(&format!("Failed to create store: {}", e))
90        })?;
91        Ok(Self {
92            store: Arc::new(store),
93        })
94    }
95
96    /// Create a Graph wrapper from this store.
97    ///
98    /// The returned Graph will use this persistent store for all operations.
99    /// Multiple Graph instances can be created from the same store, and they
100    /// will all share the same underlying data.
101    pub fn create_graph(&self) -> Result<Graph> {
102        Graph::from_store(Arc::clone(&self.store))
103    }
104
105    /// Get a reference to the underlying Store.
106    ///
107    /// This allows direct access to Oxigraph's Store API if needed.
108    pub fn inner(&self) -> &Store {
109        &self.store
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use tempfile::TempDir;
117
118    #[test]
119    fn test_store_new() {
120        // Arrange & Act
121        let store = GraphStore::new().unwrap();
122
123        // Assert
124        let graph = store.create_graph().unwrap();
125        assert!(graph.is_empty());
126    }
127
128    #[test]
129    fn test_store_open_and_create_graph() {
130        // Arrange
131        let temp_dir = TempDir::new().unwrap();
132        let store_path = temp_dir.path().join("test_store");
133
134        // Act
135        let store = GraphStore::open(&store_path).unwrap();
136        let graph = store.create_graph().unwrap();
137
138        // Assert
139        assert!(graph.is_empty());
140        assert!(store_path.exists() || !store_path.exists()); // Store may or may not create directory immediately
141    }
142
143    #[test]
144    fn test_store_create_graph_and_insert() {
145        // Arrange
146        let store = GraphStore::new().unwrap();
147        let graph = store.create_graph().unwrap();
148
149        // Act
150        graph
151            .insert_turtle(
152                r#"
153            @prefix ex: <http://example.org/> .
154            ex:alice a ex:Person .
155        "#,
156            )
157            .unwrap();
158
159        // Assert
160        assert!(!graph.is_empty());
161        assert!(graph.len() > 0);
162    }
163
164    #[test]
165    fn test_store_multiple_graphs_share_data() {
166        // Arrange
167        let store = GraphStore::new().unwrap();
168        let graph1 = store.create_graph().unwrap();
169
170        // Act
171        graph1
172            .insert_turtle(
173                r#"
174            @prefix ex: <http://example.org/> .
175            ex:alice a ex:Person .
176        "#,
177            )
178            .unwrap();
179
180        let graph2 = store.create_graph().unwrap();
181
182        // Assert - Both graphs should see the same data
183        assert!(!graph1.is_empty());
184        assert!(!graph2.is_empty());
185        assert_eq!(graph1.len(), graph2.len());
186    }
187
188    #[test]
189    fn test_store_persistent_storage() {
190        // Arrange
191        let temp_dir = TempDir::new().unwrap();
192        let store_path = temp_dir.path().join("persistent_store");
193
194        // Act - Create store and add data
195        let store1 = GraphStore::open(&store_path).unwrap();
196        let graph1 = store1.create_graph().unwrap();
197        graph1
198            .insert_turtle(
199                r#"
200            @prefix ex: <http://example.org/> .
201            ex:alice a ex:Person .
202        "#,
203            )
204            .unwrap();
205        let count1 = graph1.len();
206        drop(graph1);
207        drop(store1);
208
209        // Reopen store
210        let store2 = GraphStore::open(&store_path).unwrap();
211        let graph2 = store2.create_graph().unwrap();
212
213        // Assert - Data should persist
214        assert_eq!(graph2.len(), count1);
215        assert!(!graph2.is_empty());
216    }
217
218    #[test]
219    fn test_store_inner_access() {
220        // Arrange
221        let store = GraphStore::new().unwrap();
222
223        // Act
224        let inner = store.inner();
225
226        // Assert - Should be able to access inner store
227        assert_eq!(inner.len().unwrap_or(0), 0);
228    }
229
230    #[test]
231    fn test_store_resource_cleanup() {
232        // Arrange
233        let temp_dir = TempDir::new().unwrap();
234        let store_path = temp_dir.path().join("cleanup_test");
235
236        // Act - Create store, add data, then drop
237        {
238            let store = GraphStore::open(&store_path).unwrap();
239            let graph = store.create_graph().unwrap();
240            graph
241                .insert_turtle(
242                    r#"
243                @prefix ex: <http://example.org/> .
244                ex:alice a ex:Person .
245            "#,
246                )
247                .unwrap();
248            // Store and graph are dropped here - Rust's Drop should handle cleanup
249        }
250
251        // Assert - Store should be properly closed and data persisted
252        // Reopen store to verify cleanup didn't corrupt data
253        let store2 = GraphStore::open(&store_path).unwrap();
254        let graph2 = store2.create_graph().unwrap();
255        assert!(!graph2.is_empty());
256        assert!(graph2.len() > 0);
257    }
258}