sqlcx-core 0.2.1

SQL-first cross-language type-safe code generator — core library
Documentation
use crate::error::{Result, SqlcxError};
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};

#[derive(Debug, Clone)]
pub struct MigrationFile {
    pub version: String,
    pub name: String,
    pub path: PathBuf,
    pub content: String,
    pub checksum: String,
}

pub fn compute_checksum(content: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(content.as_bytes());
    format!("{:x}", hasher.finalize())
}

pub fn parse_filename(filename: &str) -> Option<(String, String)> {
    let stem = filename.strip_suffix(".sql")?;
    let (version, name) = stem.split_once('_')?;
    if version.is_empty() || name.is_empty() {
        return None;
    }
    if !version.chars().all(|c| c.is_ascii_digit()) {
        return None;
    }
    Some((version.to_string(), name.to_string()))
}

pub fn discover_migrations(dir: &Path) -> Result<Vec<MigrationFile>> {
    if !dir.exists() {
        return Err(SqlcxError::Migrate(format!(
            "migration directory not found: {}",
            dir.display()
        )));
    }
    let mut files = Vec::new();
    for entry in std::fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        if !path.is_file() {
            continue;
        }
        let filename = match path.file_name().and_then(|n| n.to_str()) {
            Some(n) => n,
            None => continue,
        };
        let (version, name) = match parse_filename(filename) {
            Some(parts) => parts,
            None => continue,
        };
        let content = std::fs::read_to_string(&path)?;
        let checksum = compute_checksum(&content);
        files.push(MigrationFile {
            version,
            name,
            path,
            content,
            checksum,
        });
    }
    files.sort_by(|a, b| a.version.cmp(&b.version));
    Ok(files)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_filename_valid() {
        assert_eq!(
            parse_filename("20260415153000_create_users.sql"),
            Some(("20260415153000".to_string(), "create_users".to_string()))
        );
    }

    #[test]
    fn parse_filename_invalid() {
        assert_eq!(parse_filename("create_users.sql"), None);
        assert_eq!(parse_filename("20260415153000.sql"), None);
        assert_eq!(parse_filename("abc_create_users.sql"), None);
        assert_eq!(parse_filename("20260415153000_create_users.txt"), None);
    }

    #[test]
    fn checksum_deterministic() {
        assert_eq!(
            compute_checksum("CREATE TABLE users (id INT);"),
            compute_checksum("CREATE TABLE users (id INT);")
        );
    }

    #[test]
    fn discover_sorts_by_version() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(dir.path().join("20260101000000_first.sql"), "SELECT 1;").unwrap();
        std::fs::write(dir.path().join("20260202000000_second.sql"), "SELECT 2;").unwrap();
        std::fs::write(dir.path().join("not_a_migration.txt"), "ignore").unwrap();
        let files = discover_migrations(dir.path()).unwrap();
        assert_eq!(files.len(), 2);
        assert_eq!(files[0].version, "20260101000000");
        assert_eq!(files[1].version, "20260202000000");
    }
}