Skip to main content

schema_risk/
loader.rs

1//! Reads migration SQL files from disk.
2
3use crate::error::{Result, SchemaRiskError};
4use std::path::{Path, PathBuf};
5
6/// A loaded migration file ready for parsing.
7#[derive(Debug, Clone)]
8pub struct MigrationFile {
9    pub path: PathBuf,
10    pub name: String,
11    pub sql: String,
12}
13
14/// Load a single migration file.
15pub fn load_file(path: impl AsRef<Path>) -> Result<MigrationFile> {
16    let path = path.as_ref().to_path_buf();
17    if !path.exists() {
18        return Err(SchemaRiskError::InvalidMigration(format!(
19            "File not found: {}",
20            path.display()
21        )));
22    }
23
24    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
25
26    if !["sql", "psql", "pg"].contains(&ext) {
27        return Err(SchemaRiskError::InvalidMigration(format!(
28            "Expected a .sql file, got: {}",
29            path.display()
30        )));
31    }
32
33    let sql = std::fs::read_to_string(&path).map_err(SchemaRiskError::Io)?;
34    let name = path
35        .file_name()
36        .and_then(|n| n.to_str())
37        .unwrap_or("unknown")
38        .to_string();
39
40    Ok(MigrationFile { path, name, sql })
41}
42
43/// Load all SQL files matching a glob pattern (e.g. `migrations/*.sql`).
44pub fn load_glob(pattern: &str) -> Result<Vec<MigrationFile>> {
45    let paths: Vec<PathBuf> = glob::glob(pattern)
46        .map_err(|e| SchemaRiskError::InvalidMigration(e.to_string()))?
47        .filter_map(|entry| entry.ok())
48        .filter(|p| {
49            p.extension()
50                .and_then(|e| e.to_str())
51                .map(|e| ["sql", "psql", "pg"].contains(&e))
52                .unwrap_or(false)
53        })
54        .collect();
55
56    if paths.is_empty() {
57        return Err(SchemaRiskError::NoFilesFound(pattern.to_string()));
58    }
59
60    paths.iter().map(load_file).collect()
61}