use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::diff::SchemaChange;
use super::error::MigrateError;
use super::snapshot::SchemaSnapshot;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Migration {
pub name: String,
pub created_at: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub prev: Option<String>,
#[serde(default = "default_atomic")]
pub atomic: bool,
#[serde(default, skip_serializing_if = "MigrationScope::is_default")]
pub scope: MigrationScope,
pub snapshot: SchemaSnapshot,
pub forward: Vec<Operation>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum MigrationScope {
Registry,
#[default]
Tenant,
}
impl MigrationScope {
#[must_use]
pub fn is_default(&self) -> bool {
matches!(self, Self::Tenant)
}
}
fn default_atomic() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Operation {
Schema(SchemaChange),
Data(DataOp),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DataOp {
pub sql: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub reverse_sql: Option<String>,
#[serde(default = "default_reversible")]
pub reversible: bool,
}
fn default_reversible() -> bool {
true
}
pub fn load(path: &Path) -> Result<Migration, MigrateError> {
let raw = std::fs::read_to_string(path)?;
parse(&raw)
}
pub fn parse(raw: &str) -> Result<Migration, MigrateError> {
let mig: Migration = serde_json::from_str(raw)?;
validate(&mig)?;
Ok(mig)
}
pub fn write(path: &Path, migration: &Migration) -> Result<(), MigrateError> {
let raw = serde_json::to_string_pretty(migration)?;
std::fs::write(path, raw)?;
Ok(())
}
pub fn list_dir(dir: &Path) -> Result<Vec<Migration>, MigrateError> {
if !dir.exists() {
return Ok(Vec::new());
}
let mut paths: Vec<PathBuf> = std::fs::read_dir(dir)?
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("json"))
.collect();
paths.sort();
let mut out = Vec::with_capacity(paths.len());
for p in paths {
out.push(load(&p)?);
}
validate_chain(&out, &dir.display().to_string())?;
Ok(out)
}
pub fn list_dirs<I, P>(dirs: I) -> Result<Vec<Migration>, MigrateError>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
let mut out: Vec<Migration> = Vec::new();
for dir in dirs {
out.extend(list_dir(dir.as_ref())?);
}
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok(out)
}
#[must_use]
pub fn discover_migration_dirs(project_root: &Path) -> Vec<PathBuf> {
let mut out: Vec<PathBuf> = Vec::new();
let flat = project_root.join("migrations");
if flat.is_dir() {
out.push(flat);
}
if let Ok(read) = std::fs::read_dir(project_root) {
let mut app_dirs: Vec<PathBuf> = read
.filter_map(Result::ok)
.filter_map(|entry| {
let path = entry.path();
if !path.is_dir() {
return None;
}
let name = path.file_name()?.to_str()?;
if matches!(
name,
"migrations" | "target" | "src" | ".git" | "node_modules"
) || name.starts_with('.')
{
return None;
}
let candidate = path.join("migrations");
if candidate.is_dir() {
Some(candidate)
} else {
None
}
})
.collect();
app_dirs.sort();
out.extend(app_dirs);
}
out
}
pub(crate) fn validate_chain(migrations: &[Migration], origin: &str) -> Result<(), MigrateError> {
for mig in migrations {
if let Some(prev) = &mig.prev {
if !migrations.iter().any(|m| &m.name == prev) {
return Err(MigrateError::Validation(format!(
"broken migration chain: `{}` declares prev=`{prev}` but that migration is missing from {origin}",
mig.name,
)));
}
}
}
Ok(())
}
#[must_use]
pub fn extract_index(name: &str) -> Option<u32> {
let prefix: String = name.chars().take_while(char::is_ascii_digit).collect();
if prefix.is_empty() {
None
} else {
prefix.parse().ok()
}
}
fn validate(mig: &Migration) -> Result<(), MigrateError> {
for (i, op) in mig.forward.iter().enumerate() {
if let Operation::Data(d) = op {
if d.reversible && d.reverse_sql.is_none() {
return Err(MigrateError::Validation(format!(
"{}: forward[{}]: reversible=true but reverse_sql is missing",
mig.name, i,
)));
}
}
}
Ok(())
}