use anyhow::Result;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
pub fn setup_test_env() -> Result<TempDir> {
let temp_dir = tempfile::tempdir()?;
let migrations_dir = temp_dir.path().join("migrations");
fs::create_dir_all(&migrations_dir)?;
Ok(temp_dir)
}
pub fn create_test_migration(directory: &str, name: &str, migration_type: &str) -> Result<()> {
let timestamp = chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0) / 1_000_000;
let dir_path = Path::new(directory);
fs::create_dir_all(dir_path)?;
let safe_name = name.replace(' ', "_").to_lowercase();
match migration_type {
"sql" => {
let up_file = dir_path.join(format!("{}_{}.up.sql", timestamp, safe_name));
let down_file = dir_path.join(format!("{}_{}.down.sql", timestamp, safe_name));
let table_name = safe_name.replace("create_", "").replace("add_", "");
let up_content = format!(
"-- Migration: {}\n-- Version: {}\n-- Created: {}\n\n{}\n",
name,
timestamp,
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
get_up_sql_for_table(&table_name)
);
let down_content = format!(
"-- Migration: {} (rollback)\n-- Version: {}\n-- Created: {}\n\n{}\n",
name,
timestamp,
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
get_down_sql_for_table(&table_name)
);
fs::write(&up_file, up_content)?;
fs::write(&down_file, down_content)?;
}
_ => anyhow::bail!("Unknown migration type: {}", migration_type),
}
Ok(())
}
fn get_up_sql_for_table(table_name: &str) -> String {
match table_name {
"users" => {
r#"CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);"#.to_string()
}
"posts" => {
r#"CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
user_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);"#.to_string()
}
"comments" => {
r#"CREATE TABLE comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
post_id INTEGER,
user_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);"#.to_string()
}
"categories" => {
r#"CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);"#.to_string()
}
_ => {
format!(
r#"CREATE TABLE {} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);"#,
table_name
)
}
}
}
fn get_down_sql_for_table(table_name: &str) -> String {
format!("DROP TABLE IF EXISTS {};", table_name)
}
pub fn assert_migration_applied(database_url: &str, table_name: &str) -> Result<()> {
use rusqlite::Connection;
let db_path = database_url.strip_prefix("sqlite:").unwrap_or(database_url);
let conn = Connection::open(db_path)?;
let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?1")?;
let exists = stmt.exists([table_name])?;
if !exists {
anyhow::bail!("Table '{}' does not exist, migration was not applied", table_name);
}
Ok(())
}
pub fn assert_migration_rolled_back(database_url: &str, table_name: &str) -> Result<()> {
use rusqlite::Connection;
let db_path = database_url.strip_prefix("sqlite:").unwrap_or(database_url);
let conn = Connection::open(db_path)?;
let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?1")?;
let exists = stmt.exists([table_name])?;
if exists {
anyhow::bail!("Table '{}' still exists, migration was not rolled back", table_name);
}
Ok(())
}
pub fn create_test_config(migrations_dir: &str) -> parsql_cli::config::Config {
parsql_cli::config::Config::default_with_directory(migrations_dir)
}
pub fn count_migration_files(directory: &str) -> Result<usize> {
let migrations_dir = Path::new(directory);
if !migrations_dir.exists() {
return Ok(0);
}
let count = fs::read_dir(migrations_dir)?
.filter_map(|entry| entry.ok())
.filter(|entry| {
let binding = entry.file_name();
let file_name = binding.to_string_lossy();
file_name.ends_with(".up.sql") || file_name.ends_with(".down.sql")
})
.count();
Ok(count)
}
pub fn get_migration_versions(directory: &str) -> Result<Vec<i64>> {
let migrations_dir = Path::new(directory);
let mut versions = Vec::new();
if !migrations_dir.exists() {
return Ok(versions);
}
for entry in fs::read_dir(migrations_dir)? {
let entry = entry?;
let file_name = entry.file_name().to_string_lossy().to_string();
if file_name.ends_with(".up.sql") {
let parts: Vec<&str> = file_name.splitn(2, '_').collect();
if let Ok(version) = parts[0].parse::<i64>() {
versions.push(version);
}
}
}
versions.sort();
versions.dedup();
Ok(versions)
}