use crate::{compat::text_value, database::Database, error::Error};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Migration {
pub id: String,
pub name: String,
pub sql: String,
pub created_at: DateTime<Utc>,
pub executed_at: Option<DateTime<Utc>>,
}
pub struct MigrationManager {
db: Database,
}
impl MigrationManager {
pub fn new(db: Database) -> Self {
Self { db }
}
pub async fn init(&self) -> Result<(), Error> {
let sql = r#"
CREATE TABLE IF NOT EXISTS migrations (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
sql TEXT NOT NULL,
created_at TEXT NOT NULL,
executed_at TEXT
)
"#;
let params = vec![];
self.db.execute(sql, params).await?;
Ok(())
}
pub fn create_migration(name: &str, sql: &str) -> Migration {
Migration {
id: uuid::Uuid::new_v4().to_string(),
name: name.to_string(),
sql: sql.to_string(),
created_at: Utc::now(),
executed_at: None,
}
}
pub async fn get_migrations(&self) -> Result<Vec<Migration>, Error> {
#[cfg(not(feature = "libsql"))]
{
return Ok(vec![]);
}
#[cfg(feature = "libsql")]
{
let sql =
"SELECT id, name, sql, created_at, executed_at FROM migrations ORDER BY created_at";
let mut rows = self.db.query(sql, vec![]).await?;
let mut migrations = Vec::new();
while let Some(row) = rows.next().await? {
let migration = Migration {
id: row.get(0)?,
name: row.get(1)?,
sql: row.get(2)?,
created_at: DateTime::parse_from_rfc3339(
&row.get::<String>(3).unwrap_or_default(),
)
.map_err(|_| Error::DatabaseError("Invalid datetime format".to_string()))?
.with_timezone(&Utc),
executed_at: row
.get::<Option<String>>(4)
.unwrap_or(None)
.map(|dt| {
DateTime::parse_from_rfc3339(&dt)
.map_err(|_| {
Error::DatabaseError("Invalid datetime format".to_string())
})
.map(|dt| dt.with_timezone(&Utc))
})
.transpose()?,
};
migrations.push(migration);
}
Ok(migrations)
}
}
pub async fn execute_migration(&self, migration: &Migration) -> Result<(), Error> {
self.db.execute("BEGIN", vec![]).await?;
self.db.execute(&migration.sql, vec![]).await?;
let sql = r#"
INSERT INTO migrations (id, name, sql, created_at, executed_at)
VALUES (?, ?, ?, ?, ?)
"#;
self.db
.execute(
sql,
vec![
text_value(migration.id.clone()),
text_value(migration.name.clone()),
text_value(migration.sql.clone()),
text_value(migration.created_at.to_rfc3339()),
text_value(Utc::now().to_rfc3339()),
],
)
.await?;
self.db.execute("COMMIT", vec![]).await?;
Ok(())
}
pub async fn rollback_migration(&self, migration_id: &str) -> Result<(), Error> {
let sql = "DELETE FROM migrations WHERE id = ?";
self.db
.execute(sql, vec![text_value(migration_id.to_string())])
.await?;
Ok(())
}
pub async fn get_pending_migrations(&self) -> Result<Vec<Migration>, Error> {
let migrations = self.get_migrations().await?;
Ok(migrations
.into_iter()
.filter(|m| m.executed_at.is_none())
.collect())
}
pub async fn get_executed_migrations(&self) -> Result<Vec<Migration>, Error> {
let migrations = self.get_migrations().await?;
Ok(migrations
.into_iter()
.filter(|m| m.executed_at.is_some())
.collect())
}
pub async fn run_migrations(&self, migrations: Vec<Migration>) -> Result<(), Error> {
for migration in migrations {
if let Some(_executed_at) = migration.executed_at {
continue;
}
self.execute_migration(&migration).await?;
}
Ok(())
}
pub async fn create_migration_from_file(
name: &str,
file_path: &str,
) -> Result<Migration, Error> {
let sql = std::fs::read_to_string(file_path)
.map_err(|e| Error::DatabaseError(format!("Failed to read migration file: {e}")))?;
Ok(Self::create_migration(name, &sql))
}
pub fn generate_migration_name(description: &str) -> String {
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
let sanitized_description = description
.to_lowercase()
.replace(" ", "_")
.replace("-", "_")
.chars()
.filter(|c| c.is_alphanumeric() || *c == '_')
.collect::<String>();
format!("{timestamp}_{sanitized_description}")
}
pub fn database(&self) -> &Database {
&self.db
}
}
pub struct MigrationBuilder {
name: String,
up_sql: String,
down_sql: Option<String>,
}
impl MigrationBuilder {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
up_sql: String::new(),
down_sql: None,
}
}
pub fn up(mut self, sql: &str) -> Self {
self.up_sql = sql.to_string();
self
}
pub fn down(mut self, sql: &str) -> Self {
self.down_sql = Some(sql.to_string());
self
}
pub fn build(self) -> Migration {
Migration {
id: uuid::Uuid::new_v4().to_string(),
name: self.name,
sql: self.up_sql,
created_at: Utc::now(),
executed_at: None,
}
}
}
pub mod templates {
use super::*;
pub fn create_table(table_name: &str, columns: &[(&str, &str)]) -> Migration {
let column_definitions = columns
.iter()
.map(|(name, definition)| format!("{name} {definition}"))
.collect::<Vec<_>>()
.join(", ");
let sql = format!("CREATE TABLE {table_name} ({column_definitions})");
MigrationBuilder::new(&format!("create_table_{table_name}"))
.up(&sql)
.build()
}
pub fn add_column(table_name: &str, column_name: &str, definition: &str) -> Migration {
let sql = format!("ALTER TABLE {table_name} ADD COLUMN {column_name} {definition}");
MigrationBuilder::new(&format!("add_column_{table_name}_{column_name}"))
.up(&sql)
.build()
}
pub fn drop_column(table_name: &str, column_name: &str) -> Migration {
let sql = format!("ALTER TABLE {table_name} DROP COLUMN {column_name}");
MigrationBuilder::new(&format!("drop_column_{table_name}_{column_name}"))
.up(&sql)
.build()
}
pub fn create_index(index_name: &str, table_name: &str, columns: &[&str]) -> Migration {
let column_list = columns.join(", ");
let sql = format!("CREATE INDEX {index_name} ON {table_name} ({column_list})");
MigrationBuilder::new(&format!("create_index_{index_name}"))
.up(&sql)
.build()
}
pub fn drop_index(index_name: &str) -> Migration {
let sql = format!("DROP INDEX {index_name}");
MigrationBuilder::new(&format!("drop_index_{index_name}"))
.up(&sql)
.build()
}
}