//! Database migration binary. //! //! Scans all session directories and initializes or upgrades the SQLite //! schema for each one. Standalone CLI tool invoked as `cargo run --bin migrate`. use std::path::Path; fn main() -> anyhow::Result<()> { let store = zesdex_domain::core::Store::new(); let sessions_dir = store.base_dir.join("sessions"); if !sessions_dir.exists() { tracing::info!("No sessions directory found, nothing to migrate"); return Ok(()); } let mut migrated = 0u32; let mut failed = 0u32; for entry in std::fs::read_dir(&sessions_dir)? { let entry = entry?; let path = entry.path(); if !path.is_dir() { continue; } match migrate_session_msglog(&path) { Ok(_) => { migrated += 1; tracing::info!("Migrated session: {:?}", path.file_name()); } Err(e) => { failed += 1; tracing::error!("Failed to migrate session {:?}: {e}", path.file_name()); } } } tracing::info!("Migration complete: {migrated} succeeded, {failed} failed"); if failed > 0 { anyhow::bail!("{failed} session(s) failed to migrate"); } Ok(()) } fn migrate_session_msglog(session_dir: &Path) -> anyhow::Result<()> { let msglog_path = session_dir.join("messages.sqlite"); if let Some(parent) = msglog_path.parent() { std::fs::create_dir_all(parent)?; } let conn = rusqlite::Connection::open(&msglog_path)?; conn.execute_batch("PRAGMA journal_mode = WAL;")?; conn.execute_batch("PRAGMA busy_timeout = 5000;")?; conn.execute_batch("PRAGMA foreign_keys = ON;")?; conn.execute_batch( "CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT, tool_call_id TEXT, tool_name TEXT, tool_arguments TEXT, created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS archives ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL UNIQUE, title TEXT, model TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, message_count INTEGER DEFAULT 0, token_count INTEGER DEFAULT 0, summary TEXT ); CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id); CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at); CREATE INDEX IF NOT EXISTS idx_archives_created_at ON archives(created_at); CREATE TABLE IF NOT EXISTS blobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, blob_key TEXT NOT NULL, data BLOB NOT NULL, mime_type TEXT, created_at INTEGER NOT NULL, UNIQUE(session_id, blob_key) );", )?; let version: i32 = conn .pragma_query_value(None, "user_version", |row| row.get(0)) .unwrap_or(0); if version < 1 { conn.pragma_update(None, "user_version", 1)?; } if version < 2 { conn.execute_batch( "CREATE INDEX IF NOT EXISTS idx_messages_session_role ON messages(session_id, role);", )?; conn.pragma_update(None, "user_version", 2)?; } Ok(()) }