1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
use crate::migrator::{
Migration, MigratorError, compute_hash, parse_timestamp_from_tag, split_statements,
};
use std::path::PathBuf;
/// Filesystem migration discovery.
///
/// This is intended for build-time usage (`build.rs`, proc macros) where
/// migrations are discovered once and then embedded.
#[derive(Debug, Clone)]
pub struct MigrationDir {
path: PathBuf,
}
impl MigrationDir {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
/// Discover all migrations in this directory.
///
/// Subdirectories with neither `migration.sql` nor `snapshot.json`
/// (editor artifacts, backup folders, staging leftovers) are not
/// migrations and are skipped, the same way build-time discovery skips
/// them. A folder with a snapshot but no SQL is a torn migration and
/// fails closed.
///
/// # Errors
///
/// Returns [`MigratorError::JournalError`] if a legacy `meta/_journal.json`
/// is found (run `drizzle upgrade` to convert the folder layout),
/// [`MigratorError::IoError`] if reading the directory fails, or
/// [`MigratorError::MissingMigration`] if a migration folder has a
/// `snapshot.json` but lacks its `migration.sql`.
pub fn discover(&self) -> Result<Vec<Migration>, MigratorError> {
if !self.path.exists() {
return Ok(Vec::new());
}
let journal_path = self.path.join("meta").join("_journal.json");
if journal_path.exists() {
return Err(MigratorError::JournalError(
"We detected old drizzle-kit migration folders. Upgrade them before loading migrations."
.to_string(),
));
}
self.discover_v3()
}
fn discover_v3(&self) -> Result<Vec<Migration>, MigratorError> {
use std::fs;
let mut entries = Vec::new();
for entry in fs::read_dir(&self.path).map_err(|e| MigratorError::IoError(e.to_string()))? {
let entry = entry.map_err(|e| MigratorError::IoError(e.to_string()))?;
let file_type = entry
.file_type()
.map_err(|e| MigratorError::IoError(e.to_string()))?;
if !file_type.is_dir() {
continue;
}
let tag = entry.file_name().to_string_lossy().to_string();
let path = entry.path();
let sql_path = path.join("migration.sql");
if !sql_path.is_file() {
// A folder with a snapshot but no SQL is a torn migration —
// fail closed. Anything else is not a migration folder.
if path.join("snapshot.json").is_file() {
return Err(MigratorError::MissingMigration(tag));
}
continue;
}
entries.push((tag, sql_path));
}
entries.sort_by(|a, b| a.0.cmp(&b.0));
let mut migrations = Vec::with_capacity(entries.len());
for (tag, sql_path) in entries {
let sql_content =
fs::read_to_string(&sql_path).map_err(|e| MigratorError::IoError(e.to_string()))?;
let hash = compute_hash(&sql_content);
let created_at = parse_timestamp_from_tag(&tag);
let statements = split_statements(&sql_content);
migrations.push(Migration::with_hash(tag, hash, created_at, statements));
}
Ok(migrations)
}
}