@@ -47,6 +47,18 @@ pub struct EdgeStats {
4747 pub isolated_file_count : usize ,
4848}
4949
50+ /// A record of a PARA migration operation (batch file moves).
51+ #[ derive( Debug , Clone ) ]
52+ pub struct MigrationEntry {
53+ pub id : i64 ,
54+ pub migration_id : String ,
55+ pub old_path : String ,
56+ pub new_path : String ,
57+ pub category : String ,
58+ pub confidence : f64 ,
59+ pub migrated_at : String ,
60+ }
61+
5062/// A record representing a CLI event (for observability/analytics).
5163#[ derive( Debug , Clone ) ]
5264pub struct CliEvent {
@@ -322,6 +334,20 @@ impl Store {
322334 CREATE INDEX IF NOT EXISTS idx_unresolved_source ON unresolved_links(source_file);" ,
323335 ) ?;
324336
337+ // Migration log table — records PARA migration batch operations.
338+ self . conn . execute_batch (
339+ "CREATE TABLE IF NOT EXISTS migration_log (
340+ id INTEGER PRIMARY KEY,
341+ migration_id TEXT NOT NULL,
342+ old_path TEXT NOT NULL,
343+ new_path TEXT NOT NULL,
344+ category TEXT NOT NULL,
345+ confidence REAL NOT NULL,
346+ migrated_at TEXT NOT NULL DEFAULT (datetime('now'))
347+ );
348+ CREATE INDEX IF NOT EXISTS idx_migration_id ON migration_log(migration_id);" ,
349+ ) ?;
350+
325351 Ok ( ( ) )
326352 }
327353
@@ -1504,6 +1530,68 @@ impl Store {
15041530 Ok ( results)
15051531 }
15061532
1533+ // ── Migration Log ────────────────────────────────────────────
1534+
1535+ /// Record a single file move as part of a named migration batch.
1536+ pub fn log_migration (
1537+ & self ,
1538+ migration_id : & str ,
1539+ old_path : & str ,
1540+ new_path : & str ,
1541+ category : & str ,
1542+ confidence : f64 ,
1543+ ) -> Result < ( ) > {
1544+ self . conn . execute (
1545+ "INSERT INTO migration_log (migration_id, old_path, new_path, category, confidence)
1546+ VALUES (?1, ?2, ?3, ?4, ?5)" ,
1547+ params ! [ migration_id, old_path, new_path, category, confidence] ,
1548+ ) ?;
1549+ Ok ( ( ) )
1550+ }
1551+
1552+ /// Retrieve all entries for a migration, ordered by insertion order.
1553+ pub fn get_migration ( & self , migration_id : & str ) -> Result < Vec < MigrationEntry > > {
1554+ let mut stmt = self . conn . prepare (
1555+ "SELECT id, migration_id, old_path, new_path, category, confidence, migrated_at
1556+ FROM migration_log WHERE migration_id = ?1 ORDER BY id ASC" ,
1557+ ) ?;
1558+ let rows = stmt. query_map ( params ! [ migration_id] , |row| {
1559+ Ok ( MigrationEntry {
1560+ id : row. get ( 0 ) ?,
1561+ migration_id : row. get ( 1 ) ?,
1562+ old_path : row. get ( 2 ) ?,
1563+ new_path : row. get ( 3 ) ?,
1564+ category : row. get ( 4 ) ?,
1565+ confidence : row. get ( 5 ) ?,
1566+ migrated_at : row. get ( 6 ) ?,
1567+ } )
1568+ } ) ?;
1569+ let results: Result < Vec < _ > , _ > = rows. collect ( ) ;
1570+ Ok ( results?)
1571+ }
1572+
1573+ /// Return the migration_id of the most recently created migration, if any.
1574+ pub fn get_last_migration_id ( & self ) -> Result < Option < String > > {
1575+ let result = self
1576+ . conn
1577+ . query_row (
1578+ "SELECT migration_id FROM migration_log ORDER BY migrated_at DESC, id DESC LIMIT 1" ,
1579+ [ ] ,
1580+ |row| row. get :: < _ , String > ( 0 ) ,
1581+ )
1582+ . optional ( ) ?;
1583+ Ok ( result)
1584+ }
1585+
1586+ /// Delete all entries for a migration (for undo / rollback support).
1587+ pub fn delete_migration ( & self , migration_id : & str ) -> Result < ( ) > {
1588+ self . conn . execute (
1589+ "DELETE FROM migration_log WHERE migration_id = ?1" ,
1590+ params ! [ migration_id] ,
1591+ ) ?;
1592+ Ok ( ( ) )
1593+ }
1594+
15071595 // ── Helpers ─────────────────────────────────────────────────
15081596
15091597 pub fn next_vector_id ( & self ) -> Result < u64 > {
@@ -3310,4 +3398,31 @@ mod tests {
33103398 . unwrap ( ) ;
33113399 assert_eq ! ( store. count_files_with_dates( ) . unwrap( ) , 2 ) ;
33123400 }
3401+
3402+ #[ test]
3403+ fn test_migration_log_insert_and_query ( ) {
3404+ let store = Store :: open_memory ( ) . unwrap ( ) ;
3405+ store. log_migration ( "mig-001" , "old/note.md" , "01-Projects/note.md" , "project" , 0.9 ) . unwrap ( ) ;
3406+ store. log_migration ( "mig-001" , "old/ref.md" , "03-Resources/ref.md" , "resource" , 0.85 ) . unwrap ( ) ;
3407+ let entries = store. get_migration ( "mig-001" ) . unwrap ( ) ;
3408+ assert_eq ! ( entries. len( ) , 2 ) ;
3409+ assert_eq ! ( entries[ 0 ] . old_path, "old/note.md" ) ;
3410+ }
3411+
3412+ #[ test]
3413+ fn test_migration_log_get_last ( ) {
3414+ let store = Store :: open_memory ( ) . unwrap ( ) ;
3415+ store. log_migration ( "mig-001" , "a.md" , "01-Projects/a.md" , "project" , 0.9 ) . unwrap ( ) ;
3416+ store. log_migration ( "mig-002" , "b.md" , "02-Areas/b.md" , "area" , 0.8 ) . unwrap ( ) ;
3417+ let last_id = store. get_last_migration_id ( ) . unwrap ( ) ;
3418+ assert_eq ! ( last_id. as_deref( ) , Some ( "mig-002" ) ) ;
3419+ }
3420+
3421+ #[ test]
3422+ fn test_migration_log_delete ( ) {
3423+ let store = Store :: open_memory ( ) . unwrap ( ) ;
3424+ store. log_migration ( "mig-001" , "a.md" , "01-Projects/a.md" , "project" , 0.9 ) . unwrap ( ) ;
3425+ store. delete_migration ( "mig-001" ) . unwrap ( ) ;
3426+ assert ! ( store. get_migration( "mig-001" ) . unwrap( ) . is_empty( ) ) ;
3427+ }
33133428}
0 commit comments