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}