siderite_migrations/
loader.rs1use crate::error::MigrationError;
4use crate::migration::Migration;
5use crate::state::ProjectState;
6use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
7use std::fs;
8use std::path::{Path, PathBuf};
9
10pub fn load_dir(dir: &Path) -> Result<Vec<Migration>, MigrationError> {
16 if !dir.exists() {
17 return Ok(Vec::new());
18 }
19 let mut files: Vec<PathBuf> = fs::read_dir(dir)?
20 .filter_map(|entry| {
21 let entry = entry.ok()?;
22 let path = entry.path();
23 if path.extension().is_some_and(|e| e == "json") {
24 Some(path)
25 } else {
26 None
27 }
28 })
29 .collect();
30 files.sort();
31 let mut migrations = Vec::with_capacity(files.len());
32 for path in files {
33 let text = fs::read_to_string(&path)?;
34 let mut migration: Migration = serde_json::from_str(&text)?;
35 migration.checksum = migration.compute_checksum()?;
36 migrations.push(migration);
37 }
38 Ok(migrations)
39}
40
41pub fn write_migration(dir: &Path, migration: &Migration) -> Result<PathBuf, MigrationError> {
46 fs::create_dir_all(dir)?;
47 let path = dir.join(format!("{}.json", migration.id));
48 let mut body = serde_json::to_string_pretty(migration)?;
49 if !body.ends_with('\n') {
50 body.push('\n');
51 }
52 fs::write(&path, body)?;
53 Ok(path)
54}
55
56#[derive(Debug, Clone)]
58pub struct MigrationGraph {
59 pub all: Vec<Migration>,
61 pub order: Vec<String>,
63 pub replaced_by: HashMap<String, String>,
65}
66
67impl MigrationGraph {
68 pub fn build(migrations: Vec<Migration>) -> Result<Self, MigrationError> {
75 let by_id: BTreeMap<String, Migration> = {
76 let mut m = BTreeMap::new();
77 for migration in &migrations {
78 m.insert(migration.id.clone(), migration.clone());
79 }
80 m
81 };
82 let mut replaced_by: HashMap<String, String> = HashMap::new();
83 for migration in &migrations {
84 for old in &migration.replaces {
85 replaced_by.insert(old.clone(), migration.id.clone());
86 }
87 }
88 let visible: BTreeSet<String> = by_id
89 .keys()
90 .filter(|id| !replaced_by.contains_key(id.as_str()))
91 .cloned()
92 .collect();
93
94 for id in &visible {
95 let migration = &by_id[id];
96 for dep in &migration.dependencies {
97 let resolved = resolve_dep(dep, &replaced_by);
98 if !by_id.contains_key(dep) && !by_id.contains_key(&resolved) {
99 return Err(MigrationError::MissingDependency {
100 id: id.clone(),
101 dependency: dep.clone(),
102 });
103 }
104 }
105 }
106
107 let mut deps: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
108 for id in &visible {
109 let mut d = BTreeSet::new();
110 for dep in &by_id[id].dependencies {
111 let resolved = resolve_dep(dep, &replaced_by);
112 if visible.contains(&resolved) {
113 d.insert(resolved);
114 }
115 }
116 deps.insert(id.clone(), d);
117 }
118
119 let order = topo_ids(&visible, &deps)?;
120 let heads: Vec<String> = {
121 let mut incoming: BTreeMap<String, usize> =
122 visible.iter().map(|id| (id.clone(), 0usize)).collect();
123 for id in &visible {
124 for dep in &deps[id] {
125 if let Some(n) = incoming.get_mut(dep) {
126 *n += 1;
127 }
128 }
129 }
130 let mut heads: Vec<String> = incoming
131 .into_iter()
132 .filter(|(_, n)| *n == 0)
133 .map(|(id, _)| id)
134 .collect();
135 heads.sort();
136 heads
137 };
138 if heads.len() > 1 {
139 return Err(MigrationError::MultipleHeads { heads });
140 }
141
142 Ok(Self {
143 all: migrations,
144 order,
145 replaced_by,
146 })
147 }
148
149 pub fn get(&self, id: &str) -> Option<&Migration> {
151 self.all.iter().find(|m| m.id == id)
152 }
153
154 pub fn visible(&self) -> impl Iterator<Item = &Migration> {
156 self.order.iter().filter_map(|id| self.get(id))
157 }
158
159 pub fn project_state(&self) -> Result<ProjectState, MigrationError> {
164 let mut state = ProjectState::new();
165 for migration in self.visible() {
166 for op in &migration.operations {
167 op.apply_to_state(&mut state)?;
168 }
169 }
170 Ok(state)
171 }
172}
173
174fn resolve_dep(dep: &str, replaced_by: &HashMap<String, String>) -> String {
175 replaced_by
176 .get(dep)
177 .cloned()
178 .unwrap_or_else(|| dep.to_owned())
179}
180
181fn topo_ids(
182 names: &BTreeSet<String>,
183 deps: &BTreeMap<String, BTreeSet<String>>,
184) -> Result<Vec<String>, MigrationError> {
185 let mut remaining: BTreeMap<String, usize> = names
186 .iter()
187 .map(|id| (id.clone(), deps.get(id).map(BTreeSet::len).unwrap_or(0)))
188 .collect();
189 let mut ready: BTreeSet<String> = remaining
190 .iter()
191 .filter(|(_, d)| **d == 0)
192 .map(|(id, _)| id.clone())
193 .collect();
194 let mut order = Vec::new();
195 let mut visited = HashSet::new();
196 while let Some(id) = ready.iter().next().cloned() {
197 ready.remove(&id);
198 order.push(id.clone());
199 visited.insert(id.clone());
200 remaining.remove(&id);
201 for (node, ds) in deps {
202 if ds.contains(&id)
203 && let Some(deg) = remaining.get_mut(node)
204 {
205 *deg = deg.saturating_sub(1);
206 if *deg == 0 {
207 ready.insert(node.clone());
208 }
209 }
210 }
211 }
212 if order.len() != names.len() {
213 let leftover: Vec<String> = names
214 .difference(&visited.into_iter().collect())
215 .cloned()
216 .collect();
217 let cycle = find_cycle(deps, &leftover).unwrap_or(leftover);
218 return Err(MigrationError::Cycle { nodes: cycle });
219 }
220 Ok(order)
221}
222
223fn find_cycle(
224 deps: &BTreeMap<String, BTreeSet<String>>,
225 leftover: &[String],
226) -> Option<Vec<String>> {
227 fn visit<'a>(
228 node: &'a str,
229 deps: &'a BTreeMap<String, BTreeSet<String>>,
230 leftover: &HashSet<&str>,
231 stack: &mut Vec<&'a str>,
232 seen: &mut HashSet<&'a str>,
233 ) -> Option<Vec<String>> {
234 if let Some(i) = stack.iter().position(|n| *n == node) {
235 let mut cycle: Vec<String> = stack[i..].iter().map(|s| (*s).to_owned()).collect();
236 cycle.push(node.to_owned());
237 return Some(cycle);
238 }
239 if !seen.insert(node) {
240 return None;
241 }
242 stack.push(node);
243 if let Some(ds) = deps.get(node) {
244 for dep in ds {
245 if leftover.contains(dep.as_str())
246 && let Some(c) = visit(dep, deps, leftover, stack, seen)
247 {
248 return Some(c);
249 }
250 }
251 }
252 stack.pop();
253 None
254 }
255 let set: HashSet<&str> = leftover.iter().map(String::as_str).collect();
256 let mut seen = HashSet::new();
257 for id in leftover {
258 if let Some(c) = visit(id, deps, &set, &mut Vec::new(), &mut seen) {
259 return Some(c);
260 }
261 }
262 None
263}
264
265pub fn is_applied(migration: &Migration, history: &HashSet<String>) -> bool {
267 if history.contains(&migration.id) {
268 return true;
269 }
270 !migration.replaces.is_empty() && migration.replaces.iter().all(|id| history.contains(id))
271}