use crate::error::{Result, SpliceError};
use magellan::CodeGraph as MagellanGraph;
use magellan::MAGELLAN_SCHEMA_VERSION;
use rusqlite::Connection;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct MigrationResult {
pub previous_version: i64,
pub new_version: i64,
pub backup_path: Option<PathBuf>,
pub symbols_migrated: usize,
}
pub fn check_schema_version(db_path: &Path) -> Result<i64> {
if !db_path.exists() {
return Err(SpliceError::Other(format!(
"Database not found: {}",
db_path.display()
)));
}
read_schema_version(db_path)
}
pub fn migrate_database(db_path: &Path, backup: bool, dry_run: bool) -> Result<MigrationResult> {
if !db_path.exists() {
return Err(SpliceError::Other(format!(
"Database not found: {}",
db_path.display()
)));
}
let previous_version = check_schema_version(db_path)?;
if dry_run {
return Ok(MigrationResult {
previous_version,
new_version: MAGELLAN_SCHEMA_VERSION,
backup_path: None,
symbols_migrated: 0,
});
}
let backup_path = if backup {
Some(create_backup(db_path, previous_version)?)
} else {
None
};
let db_path_str = db_path
.to_str()
.ok_or_else(|| SpliceError::Other(format!("Invalid UTF-8 in path: {:?}", db_path)))?;
let _graph = MagellanGraph::open(db_path_str).map_err(|e| SpliceError::Magellan {
context: format!("Failed to open database at {}", db_path_str),
source: e,
})?;
let new_version = check_schema_version(db_path)?;
let symbols_migrated = 0;
Ok(MigrationResult {
previous_version,
new_version,
backup_path,
symbols_migrated,
})
}
pub fn create_backup(db_path: &Path, schema_version: i64) -> Result<PathBuf> {
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
let file_name = db_path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| SpliceError::Other(format!("Invalid UTF-8 in path: {:?}", db_path)))?;
let backup_path =
db_path.with_file_name(format!("{file_name}.backup.v{schema_version}.{timestamp}"));
std::fs::copy(db_path, &backup_path).map_err(|e| SpliceError::Io {
path: backup_path.clone(),
source: e,
})?;
Ok(backup_path)
}
fn read_schema_version(db_path: &Path) -> Result<i64> {
let conn = Connection::open(db_path).map_err(|e| {
SpliceError::Other(format!(
"Failed to open database {}: {}",
db_path.display(),
e
))
})?;
conn.query_row(
"SELECT magellan_schema_version FROM magellan_meta WHERE id = 1",
[],
|row| row.get(0),
)
.map_err(|e| {
SpliceError::Other(format!(
"Failed to read magellan schema version from {}: {}",
db_path.display(),
e
))
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_check_schema_version_nonexistent() {
let temp_db = std::env::temp_dir().join("nonexistent_test_db.db");
let _ = fs::remove_file(&temp_db);
let result = check_schema_version(&temp_db);
assert!(result.is_err());
}
#[test]
fn test_create_backup() {
let temp_dir = std::env::temp_dir();
let test_db = temp_dir.join("test_backup.db");
let _ = fs::remove_file(&test_db);
fs::write(&test_db, b"test data").unwrap();
let result = create_backup(&test_db, 20);
assert!(result.is_ok());
let backup_db = result.unwrap();
assert!(backup_db.exists());
assert!(
backup_db
.file_name()
.and_then(|name| name.to_str())
.unwrap()
.starts_with("test_backup.db.backup.v20."),
"unexpected backup name: {}",
backup_db.display()
);
assert_eq!(fs::read_to_string(&backup_db).unwrap(), "test data");
let _ = fs::remove_file(&test_db);
let _ = fs::remove_file(&backup_db);
}
#[test]
fn test_migrate_database_dry_run() {
let temp_db = std::env::temp_dir().join("test_dry_run.db");
let _ = fs::remove_file(&temp_db);
let conn = Connection::open(&temp_db).unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, 19, 0, 0)",
[],
)
.unwrap();
drop(conn);
let result = migrate_database(&temp_db, false, true);
assert!(result.is_ok());
let migration_result = result.unwrap();
assert_eq!(migration_result.previous_version, 19);
assert_eq!(migration_result.new_version, MAGELLAN_SCHEMA_VERSION);
assert!(migration_result.backup_path.is_none());
assert_eq!(migration_result.symbols_migrated, 0);
let _ = fs::remove_file(&temp_db);
}
}