1use std::collections::{BTreeSet, HashMap};
4
5use crate::graphs::MigrationGraph;
6use crate::migrations::Migration;
7use crate::operations::Operation;
8use crate::states::types::EntityKind;
9use crate::states::{ReplayError, Schema};
10
11#[derive(Debug)]
13pub struct ReplaySources {
14 pub schema: Schema,
16 pub last_per_ns: HashMap<String, String>,
18 pub entity_ns: HashMap<(EntityKind, String), String>,
20}
21
22#[derive(Debug)]
24pub struct ReplayEngine<'a> {
25 graph: &'a MigrationGraph,
26}
27
28impl<'a> ReplayEngine<'a> {
29 pub fn new(graph: &'a MigrationGraph) -> Self {
31 Self { graph }
32 }
33
34 pub fn replay_ids(&self, ids: &[String]) -> Result<Schema, ReplayError> {
36 let mut schema = Schema::default();
37 for id in ids {
38 let migration = self
39 .graph
40 .get(id)
41 .ok_or_else(|| ReplayError::MigrationNotFound(id.clone()))?;
42 Self::apply_migration(&mut schema, migration)?;
43 }
44 Ok(schema)
45 }
46
47 pub fn replay_with_sources(
49 &self,
50 ordered_ids: &[String],
51 ) -> Result<ReplaySources, ReplayError> {
52 let mut schema = Schema::default();
53 let mut last_per_ns = HashMap::new();
54 let mut entity_ns = HashMap::new();
55
56 for id in ordered_ids {
57 let migration = self
58 .graph
59 .get(id)
60 .ok_or_else(|| ReplayError::MigrationNotFound(id.clone()))?;
61 Self::apply_migration(&mut schema, migration)?;
62 let ns = namespace_of(id).to_string();
63 for entity in migration.get_entities() {
64 entity_ns.insert(entity, ns.clone());
65 }
66 last_per_ns.insert(ns, id.clone());
67 }
68
69 Ok(ReplaySources {
70 schema,
71 last_per_ns,
72 entity_ns,
73 })
74 }
75
76 pub fn apply_migration(schema: &mut Schema, migration: &Migration) -> Result<(), ReplayError> {
78 for (index, operation) in migration.operations.iter().enumerate() {
79 schema
80 .apply(operation)
81 .map_err(|inner| ReplayError::WithContext {
82 migration: migration.id.clone(),
83 op_num: index + 1,
84 inner: Box::new(inner),
85 })?;
86 }
87 Ok(())
88 }
89}
90
91pub fn namespace_of(id: &str) -> &str {
93 match id.rfind('/') {
94 Some(pos) => &id[..pos],
95 None => "",
96 }
97}
98
99pub fn compute_deps(
101 ops: &[Operation],
102 last_per_ns: &HashMap<String, String>,
103 entity_ns: &HashMap<(EntityKind, String), String>,
104) -> Vec<String> {
105 let mut namespaces = BTreeSet::new();
106 namespaces.insert(String::new());
107
108 for op in ops {
109 for entity in op_entities(op) {
110 if let Some(ns) = entity_ns.get(&entity) {
111 namespaces.insert(ns.clone());
112 }
113 }
114 }
115
116 namespaces
117 .iter()
118 .filter_map(|ns| last_per_ns.get(ns).cloned())
119 .collect()
120}
121
122pub fn deterministic_name_from_ops(ops: &[Operation]) -> String {
124 let mut labels: Vec<&str> = Vec::new();
125 for op in ops {
126 if let Some(label) = op.entity_label()
127 && !labels.contains(&label)
128 {
129 labels.push(label);
130 }
131 }
132 match labels.as_slice() {
133 [] => "changes".to_string(),
134 [a] => sanitize_id_part(a),
135 [a, b] => format!("{}_{}", sanitize_id_part(a), sanitize_id_part(b)),
136 [a, _, ..] => format!("{}_changes", sanitize_id_part(a)),
137 }
138}
139
140fn sanitize_id_part(value: &str) -> String {
141 let sanitized: String = value
142 .chars()
143 .map(|c| {
144 if c.is_ascii_lowercase() || c.is_ascii_digit() {
145 c
146 } else if c.is_ascii_alphabetic() {
147 c.to_ascii_lowercase()
148 } else {
149 '_'
150 }
151 })
152 .collect();
153 let trimmed = sanitized.trim_matches('_');
154 if trimmed.is_empty() {
155 "changes".to_string()
156 } else {
157 trimmed.to_string()
158 }
159}
160
161fn op_entities(op: &Operation) -> Vec<(EntityKind, String)> {
162 op.touched_entities()
163}