Skip to main content

rustorm_migrate/
parser.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::runner::MigrationFile;
5
6/// Парсит директорию с миграциями и возвращает отсортированный список.
7pub fn parse_migration_dir(dir: &Path) -> Result<Vec<MigrationFile>, String> {
8    if !dir.exists() {
9        return Ok(vec![]);
10    }
11
12    let mut files: Vec<MigrationFile> = vec![];
13
14    let entries = fs::read_dir(dir)
15        .map_err(|e| format!("Не удалось прочитать директорию {:?}: {}", dir, e))?;
16
17    for entry in entries.flatten() {
18        let path = entry.path();
19        if path.extension().and_then(|e| e.to_str()) != Some("sql") {
20            continue;
21        }
22
23        let filename = path
24            .file_stem()
25            .and_then(|s| s.to_str())
26            .unwrap_or("")
27            .to_string();
28
29        // Ожидаем формат: 0001_create_users_table
30        let (version, name) = parse_filename(&filename)?;
31
32        let content = fs::read_to_string(&path)
33            .map_err(|e| format!("Не удалось прочитать {:?}: {}", path, e))?;
34
35        let (up_sql, down_sql) = split_up_down(&content);
36
37        files.push(MigrationFile {
38            version,
39            name,
40            up_sql,
41            down_sql,
42            path,
43        });
44    }
45
46    // Сортируем по версии
47    files.sort_by(|a, b| a.version.cmp(&b.version));
48    Ok(files)
49}
50
51/// Парсит имя файла вида "0001_create_users_table" → ("0001", "create_users_table")
52fn parse_filename(filename: &str) -> Result<(String, String), String> {
53    let parts: Vec<&str> = filename.splitn(2, '_').collect();
54    if parts.len() < 2 {
55        // Попробуем просто взять весь filename как версию
56        return Ok((filename.to_string(), filename.to_string()));
57    }
58    Ok((parts[0].to_string(), parts[1..].join("_")))
59}
60
61/// Разделяет SQL файл на UP и DOWN части.
62/// Ищет маркеры `-- === UP ===` и `-- === DOWN ===`
63pub fn split_up_down(content: &str) -> (String, String) {
64    let up_marker = "-- === UP ===";
65    let down_marker = "-- === DOWN ===";
66
67    if let Some(up_pos) = content.find(up_marker) {
68        let after_up = &content[up_pos + up_marker.len()..];
69
70        if let Some(down_pos) = after_up.find(down_marker) {
71            let up_sql = after_up[..down_pos].trim().to_string();
72            let down_sql = after_up[down_pos + down_marker.len()..].trim().to_string();
73            return (up_sql, down_sql);
74        }
75        // Нет DOWN секции
76        return (after_up.trim().to_string(), String::new());
77    }
78
79    // Нет маркеров — весь файл считается UP
80    (content.trim().to_string(), String::new())
81}
82
83/// Генерирует имя следующего файла миграции.
84pub fn next_migration_filename(existing: &[MigrationFile], description: &str) -> String {
85    let next_num = existing.len() + 1;
86    let desc_slug = description
87        .to_lowercase()
88        .replace(' ', "_")
89        .chars()
90        .filter(|c| c.is_alphanumeric() || *c == '_')
91        .collect::<String>();
92    format!("{:04}_{}", next_num, desc_slug)
93}