Skip to content

Commit a4b629b

Browse files
committed
feat(store): add migration_log table for PARA migration tracking
1 parent 0362525 commit a4b629b

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

src/store.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
5264
pub 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

Comments
 (0)