use std::fs;
use std::path::PathBuf;
use std::process::Command;
use anyhow::{Context, Result};
use super::output_stream::OutputStreamWidget;
pub struct MigrationViewer {
migrations_dir: PathBuf,
}
impl MigrationViewer {
pub fn new(migrations_dir: PathBuf) -> Self {
Self { migrations_dir }
}
pub fn view_migration(
&self,
version: i64,
file_type: MigrationFileType,
output: &mut OutputStreamWidget,
) -> Result<String> {
let file_path = self.find_migration_file(version, file_type)?;
output.add_info(format!("Reading migration file: {}", file_path.display()));
let content = fs::read_to_string(&file_path)
.context(format!("Failed to read migration file: {}", file_path.display()))?;
Ok(content)
}
pub fn edit_migration(
&self,
version: i64,
file_type: MigrationFileType,
output: &mut OutputStreamWidget,
) -> Result<()> {
let file_path = self.find_migration_file(version, file_type)?;
let editor = std::env::var("EDITOR")
.or_else(|_| std::env::var("VISUAL"))
.unwrap_or_else(|_| {
if cfg!(windows) {
"notepad".to_string()
} else {
"vi".to_string()
}
});
output.add_info(format!("Opening {} in {}", file_path.display(), editor));
let status = Command::new(&editor)
.arg(&file_path)
.status()
.context(format!("Failed to launch editor: {}", editor))?;
if status.success() {
output.add_success(format!("Editor closed successfully"));
if file_path.exists() {
let content = fs::read_to_string(&file_path)?;
if content.trim().is_empty() {
output.add_warning("Migration file is empty after editing".to_string());
} else {
output.add_info(format!("Migration file saved: {} bytes", content.len()));
}
} else {
output.add_error("Migration file was deleted!".to_string());
}
} else {
output.add_error(format!("Editor exited with error code: {:?}", status.code()));
}
Ok(())
}
pub fn create_and_edit_migration(
&self,
_version: i64,
name: &str,
output: &mut OutputStreamWidget,
) -> Result<()> {
let migrations_dir = self.migrations_dir.clone();
let creator = super::migration_creator::MigrationCreator::new(migrations_dir);
let files = creator.create_migration(name, "sql")?;
output.add_success(format!("Created migration files for version {}", files.version));
let version_num = files.version.parse::<i64>()
.context("Invalid version number")?;
self.edit_migration(version_num, MigrationFileType::Up, output)?;
Ok(())
}
fn find_migration_file(&self, version: i64, file_type: MigrationFileType) -> Result<PathBuf> {
let suffix = match file_type {
MigrationFileType::Up => ".up.sql",
MigrationFileType::Down => ".down.sql",
};
let entries = fs::read_dir(&self.migrations_dir)
.context("Failed to read migrations directory")?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
if filename.starts_with(&version.to_string()) && filename.ends_with(suffix) {
return Ok(path);
}
}
}
anyhow::bail!(
"Migration file not found for version {} ({})",
version,
match file_type {
MigrationFileType::Up => "up",
MigrationFileType::Down => "down",
}
)
}
pub fn list_migration_files(&self) -> Result<Vec<MigrationFileInfo>> {
let mut files = Vec::new();
if !self.migrations_dir.exists() {
return Ok(files);
}
let entries = fs::read_dir(&self.migrations_dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
if filename.ends_with(".up.sql") || filename.ends_with(".down.sql") {
let metadata = fs::metadata(&path)?;
let size = metadata.len();
if let Some(underscore_pos) = filename.find('_') {
if let Ok(version) = filename[..underscore_pos].parse::<i64>() {
let is_up = filename.ends_with(".up.sql");
files.push(MigrationFileInfo {
version,
filename: filename.to_string(),
path: path.clone(),
size,
is_up,
});
}
}
}
}
}
files.sort_by_key(|f| (f.version, !f.is_up));
Ok(files)
}
}
#[derive(Debug, Clone, Copy)]
pub enum MigrationFileType {
Up,
Down,
}
#[derive(Debug, Clone)]
pub struct MigrationFileInfo {
pub version: i64,
pub filename: String,
pub path: PathBuf,
pub size: u64,
pub is_up: bool,
}