llm_memory_graph/migration.rs
1//! Migration utilities for transitioning from sync to async APIs
2//!
3//! This module provides tools and helpers for migrating from the synchronous
4//! `MemoryGraph` API to the asynchronous `AsyncMemoryGraph` API.
5//!
6//! # Migration Strategies
7//!
8//! ## 1. Gradual Migration (Recommended)
9//!
10//! Start by identifying async boundaries in your application and migrate
11//! one component at a time:
12//!
13//! ```no_run
14//! // Old sync code
15//! use llm_memory_graph::MemoryGraph;
16//!
17//! fn process_sync() -> Result<(), Box<dyn std::error::Error>> {
18//! let graph = MemoryGraph::open(Default::default())?;
19//! let session = graph.create_session()?;
20//! Ok(())
21//! }
22//!
23//! // New async code
24//! use llm_memory_graph::AsyncMemoryGraph;
25//!
26//! async fn process_async() -> Result<(), Box<dyn std::error::Error>> {
27//! let graph = AsyncMemoryGraph::open(Default::default()).await?;
28//! let session = graph.create_session().await?;
29//! Ok(())
30//! }
31//! ```
32//!
33//! ## 2. Parallel APIs
34//!
35//! Both APIs can coexist during migration. The sync and async versions
36//! use the same storage format and are fully compatible.
37//!
38//! # Data Compatibility
39//!
40//! - Same storage format (Sled-based)
41//! - No data migration required
42//! - Can switch between sync and async at any time
43//! - Node IDs and edge IDs are compatible
44
45use crate::error::Result;
46use crate::types::Config;
47use crate::{AsyncMemoryGraph, MemoryGraph};
48
49/// Migration helper providing utilities for transitioning between sync and async APIs
50pub struct MigrationHelper;
51
52impl MigrationHelper {
53 /// Verify that a database is compatible with both sync and async APIs
54 ///
55 /// This function checks that:
56 /// - The database can be opened with sync API
57 /// - The database can be opened with async API
58 /// - Both APIs can read the same data
59 ///
60 /// # Examples
61 ///
62 /// ```no_run
63 /// use llm_memory_graph::{Config, migration::MigrationHelper};
64 ///
65 /// #[tokio::main]
66 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
67 /// let config = Config::new("./data/graph.db");
68 /// MigrationHelper::verify_compatibility(&config).await?;
69 /// println!("Database is compatible!");
70 /// Ok(())
71 /// }
72 /// ```
73 pub async fn verify_compatibility(config: &Config) -> Result<CompatibilityReport> {
74 let mut report = CompatibilityReport {
75 sync_accessible: false,
76 async_accessible: false,
77 sync_node_count: 0,
78 async_node_count: 0,
79 sync_edge_count: 0,
80 async_edge_count: 0,
81 compatible: false,
82 };
83
84 // Test sync API
85 match MemoryGraph::open(config.clone()) {
86 Ok(graph) => {
87 report.sync_accessible = true;
88 if let Ok(stats) = graph.stats() {
89 report.sync_node_count = stats.node_count;
90 report.sync_edge_count = stats.edge_count;
91 }
92 }
93 Err(_) => return Ok(report),
94 }
95
96 // Test async API
97 match AsyncMemoryGraph::open(config.clone()).await {
98 Ok(graph) => {
99 report.async_accessible = true;
100 if let Ok(stats) = graph.stats().await {
101 report.async_node_count = stats.node_count;
102 report.async_edge_count = stats.edge_count;
103 }
104 }
105 Err(_) => return Ok(report),
106 }
107
108 // Verify counts match
109 report.compatible = report.sync_accessible
110 && report.async_accessible
111 && report.sync_node_count == report.async_node_count
112 && report.sync_edge_count == report.async_edge_count;
113
114 Ok(report)
115 }
116
117 /// Create a migration checkpoint that can be used to rollback if needed
118 ///
119 /// This function creates a snapshot of database statistics that can be
120 /// compared later to detect any issues during migration.
121 ///
122 /// # Examples
123 ///
124 /// ```no_run
125 /// use llm_memory_graph::{Config, migration::MigrationHelper};
126 ///
127 /// #[tokio::main]
128 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
129 /// let config = Config::new("./data/graph.db");
130 /// let checkpoint = MigrationHelper::create_checkpoint(&config).await?;
131 /// println!("Checkpoint: {} nodes, {} edges",
132 /// checkpoint.node_count, checkpoint.edge_count);
133 /// Ok(())
134 /// }
135 /// ```
136 pub async fn create_checkpoint(config: &Config) -> Result<MigrationCheckpoint> {
137 let graph = AsyncMemoryGraph::open(config.clone()).await?;
138 let stats = graph.stats().await?;
139
140 Ok(MigrationCheckpoint {
141 timestamp: chrono::Utc::now(),
142 node_count: stats.node_count,
143 edge_count: stats.edge_count,
144 session_count: stats.session_count,
145 storage_bytes: stats.storage_bytes,
146 })
147 }
148
149 /// Verify that a database hasn't been corrupted after migration
150 ///
151 /// Compares current state against a checkpoint created before migration.
152 ///
153 /// # Examples
154 ///
155 /// ```no_run
156 /// use llm_memory_graph::{Config, migration::MigrationHelper};
157 ///
158 /// #[tokio::main]
159 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
160 /// let config = Config::new("./data/graph.db");
161 /// let before = MigrationHelper::create_checkpoint(&config).await?;
162 ///
163 /// // ... perform migration ...
164 ///
165 /// let result = MigrationHelper::verify_checkpoint(&config, &before).await?;
166 /// assert!(result.valid, "Migration corrupted data!");
167 /// Ok(())
168 /// }
169 /// ```
170 pub async fn verify_checkpoint(
171 config: &Config,
172 checkpoint: &MigrationCheckpoint,
173 ) -> Result<CheckpointVerification> {
174 let graph = AsyncMemoryGraph::open(config.clone()).await?;
175 let stats = graph.stats().await?;
176
177 let verification = CheckpointVerification {
178 valid: stats.node_count >= checkpoint.node_count
179 && stats.edge_count >= checkpoint.edge_count,
180 node_count_match: stats.node_count == checkpoint.node_count,
181 edge_count_match: stats.edge_count == checkpoint.edge_count,
182 nodes_added: stats.node_count.saturating_sub(checkpoint.node_count),
183 edges_added: stats.edge_count.saturating_sub(checkpoint.edge_count),
184 };
185
186 Ok(verification)
187 }
188
189 /// Run a test migration workflow to validate the migration process
190 ///
191 /// This performs a complete migration test:
192 /// 1. Creates a checkpoint
193 /// 2. Tests sync API access
194 /// 3. Tests async API access
195 /// 4. Verifies data integrity
196 ///
197 /// # Examples
198 ///
199 /// ```no_run
200 /// use llm_memory_graph::{Config, migration::MigrationHelper};
201 ///
202 /// #[tokio::main]
203 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
204 /// let config = Config::new("./data/graph.db");
205 /// let report = MigrationHelper::run_migration_test(&config).await?;
206 /// println!("Migration test: {}", if report.success { "PASSED" } else { "FAILED" });
207 /// Ok(())
208 /// }
209 /// ```
210 pub async fn run_migration_test(config: &Config) -> Result<MigrationTestReport> {
211 let mut report = MigrationTestReport {
212 success: false,
213 steps_completed: Vec::new(),
214 errors: Vec::new(),
215 };
216
217 // Step 1: Create checkpoint
218 match Self::create_checkpoint(config).await {
219 Ok(_) => report
220 .steps_completed
221 .push("Checkpoint created".to_string()),
222 Err(e) => {
223 report
224 .errors
225 .push(format!("Failed to create checkpoint: {}", e));
226 return Ok(report);
227 }
228 }
229
230 // Step 2: Test sync API
231 match MemoryGraph::open(config.clone()) {
232 Ok(_) => report
233 .steps_completed
234 .push("Sync API accessible".to_string()),
235 Err(e) => {
236 report.errors.push(format!("Sync API failed: {}", e));
237 return Ok(report);
238 }
239 }
240
241 // Step 3: Test async API
242 match AsyncMemoryGraph::open(config.clone()).await {
243 Ok(_) => report
244 .steps_completed
245 .push("Async API accessible".to_string()),
246 Err(e) => {
247 report.errors.push(format!("Async API failed: {}", e));
248 return Ok(report);
249 }
250 }
251
252 // Step 4: Verify compatibility
253 match Self::verify_compatibility(config).await {
254 Ok(compat) if compat.compatible => {
255 report.steps_completed.push("APIs compatible".to_string());
256 }
257 Ok(_) => {
258 report.errors.push("APIs not compatible".to_string());
259 return Ok(report);
260 }
261 Err(e) => {
262 report
263 .errors
264 .push(format!("Compatibility check failed: {}", e));
265 return Ok(report);
266 }
267 }
268
269 report.success = true;
270 Ok(report)
271 }
272}
273
274/// Report on database compatibility between sync and async APIs
275#[derive(Debug, Clone)]
276pub struct CompatibilityReport {
277 /// Whether the sync API can access the database
278 pub sync_accessible: bool,
279 /// Whether the async API can access the database
280 pub async_accessible: bool,
281 /// Node count from sync API
282 pub sync_node_count: u64,
283 /// Node count from async API
284 pub async_node_count: u64,
285 /// Edge count from sync API
286 pub sync_edge_count: u64,
287 /// Edge count from async API
288 pub async_edge_count: u64,
289 /// Whether the APIs are compatible (counts match)
290 pub compatible: bool,
291}
292
293/// Snapshot of database state for migration checkpoints
294#[derive(Debug, Clone)]
295pub struct MigrationCheckpoint {
296 /// When the checkpoint was created
297 pub timestamp: chrono::DateTime<chrono::Utc>,
298 /// Number of nodes at checkpoint
299 pub node_count: u64,
300 /// Number of edges at checkpoint
301 pub edge_count: u64,
302 /// Number of sessions at checkpoint
303 pub session_count: u64,
304 /// Storage size in bytes at checkpoint
305 pub storage_bytes: u64,
306}
307
308/// Result of verifying a migration checkpoint
309#[derive(Debug, Clone)]
310pub struct CheckpointVerification {
311 /// Whether the checkpoint is valid (data not lost)
312 pub valid: bool,
313 /// Whether node count matches exactly
314 pub node_count_match: bool,
315 /// Whether edge count matches exactly
316 pub edge_count_match: bool,
317 /// Number of nodes added since checkpoint
318 pub nodes_added: u64,
319 /// Number of edges added since checkpoint
320 pub edges_added: u64,
321}
322
323/// Report from running a complete migration test
324#[derive(Debug, Clone)]
325pub struct MigrationTestReport {
326 /// Whether the migration test succeeded
327 pub success: bool,
328 /// Steps that were completed successfully
329 pub steps_completed: Vec<String>,
330 /// Errors encountered during the test
331 pub errors: Vec<String>,
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use tempfile::tempdir;
338
339 #[tokio::test]
340 async fn test_verify_compatibility_empty_db() {
341 let dir = tempdir().unwrap();
342 let config = Config::new(dir.path());
343
344 let report = MigrationHelper::verify_compatibility(&config)
345 .await
346 .unwrap();
347
348 assert!(report.sync_accessible);
349 assert!(report.async_accessible);
350 assert!(report.compatible);
351 assert_eq!(report.sync_node_count, 0);
352 assert_eq!(report.async_node_count, 0);
353 }
354
355 #[tokio::test]
356 async fn test_verify_compatibility_with_data() {
357 let dir = tempdir().unwrap();
358 let config = Config::new(dir.path());
359
360 // Create data with sync API
361 {
362 let graph = MemoryGraph::open(config.clone()).unwrap();
363 graph.create_session().unwrap();
364 }
365
366 let report = MigrationHelper::verify_compatibility(&config)
367 .await
368 .unwrap();
369
370 assert!(report.sync_accessible);
371 assert!(report.async_accessible);
372 assert!(report.compatible);
373 assert_eq!(report.sync_node_count, 1);
374 assert_eq!(report.async_node_count, 1);
375 }
376
377 #[tokio::test]
378 async fn test_create_checkpoint() {
379 let dir = tempdir().unwrap();
380 let config = Config::new(dir.path());
381
382 // Create some data
383 {
384 let graph = AsyncMemoryGraph::open(config.clone()).await.unwrap();
385 graph.create_session().await.unwrap();
386 }
387
388 let checkpoint = MigrationHelper::create_checkpoint(&config).await.unwrap();
389
390 assert_eq!(checkpoint.node_count, 1);
391 assert_eq!(checkpoint.edge_count, 0);
392 }
393
394 #[tokio::test]
395 async fn test_verify_checkpoint() {
396 let dir = tempdir().unwrap();
397 let config = Config::new(dir.path());
398
399 // Create checkpoint with initial data
400 {
401 let graph = AsyncMemoryGraph::open(config.clone()).await.unwrap();
402 graph.create_session().await.unwrap();
403 }
404
405 let checkpoint = MigrationHelper::create_checkpoint(&config).await.unwrap();
406
407 // Add more data
408 {
409 let graph = AsyncMemoryGraph::open(config.clone()).await.unwrap();
410 graph.create_session().await.unwrap();
411 }
412
413 let verification = MigrationHelper::verify_checkpoint(&config, &checkpoint)
414 .await
415 .unwrap();
416
417 assert!(verification.valid);
418 assert!(!verification.node_count_match); // We added more nodes
419 assert_eq!(verification.nodes_added, 1);
420 }
421
422 #[tokio::test]
423 async fn test_run_migration_test() {
424 let dir = tempdir().unwrap();
425 let config = Config::new(dir.path());
426
427 // Create initial data
428 {
429 let graph = MemoryGraph::open(config.clone()).unwrap();
430 graph.create_session().unwrap();
431 }
432
433 let report = MigrationHelper::run_migration_test(&config).await.unwrap();
434
435 assert!(report.success, "Migration test should succeed");
436 assert!(report.errors.is_empty(), "Should have no errors");
437 assert!(
438 report.steps_completed.len() >= 4,
439 "Should complete all steps"
440 );
441 }
442
443 #[tokio::test]
444 async fn test_sync_async_interop() {
445 let dir = tempdir().unwrap();
446 let config = Config::new(dir.path());
447
448 // Create session with sync API
449 let session_id = {
450 let graph = MemoryGraph::open(config.clone()).unwrap();
451 let session = graph.create_session().unwrap();
452 session.id
453 };
454
455 // Read session with async API
456 {
457 let graph = AsyncMemoryGraph::open(config.clone()).await.unwrap();
458 let session = graph.get_session(session_id).await.unwrap();
459 assert_eq!(session.id, session_id);
460 }
461
462 // Add prompt with async API
463 let prompt_id = {
464 let graph = AsyncMemoryGraph::open(config.clone()).await.unwrap();
465 graph
466 .add_prompt(session_id, "Test prompt".to_string(), None)
467 .await
468 .unwrap()
469 };
470
471 // Read with sync API
472 {
473 let graph = MemoryGraph::open(config.clone()).unwrap();
474 let node = graph.get_node(prompt_id).unwrap();
475 assert_eq!(node.id(), prompt_id);
476 }
477 }
478}