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}