use std::collections::{BTreeSet, HashMap};
use crate::graphs::MigrationGraph;
use crate::migrations::Migration;
use crate::operations::Operation;
use crate::states::types::EntityKind;
use crate::states::{ReplayError, Schema};
#[derive(Debug)]
pub struct ReplaySources {
pub schema: Schema,
pub last_per_ns: HashMap<String, String>,
pub entity_ns: HashMap<(EntityKind, String), String>,
}
#[derive(Debug)]
pub struct ReplayEngine<'a> {
graph: &'a MigrationGraph,
}
impl<'a> ReplayEngine<'a> {
pub fn new(graph: &'a MigrationGraph) -> Self {
Self { graph }
}
pub fn replay_ids(&self, ids: &[String]) -> Result<Schema, ReplayError> {
let mut schema = Schema::default();
for id in ids {
let migration = self
.graph
.get(id)
.ok_or_else(|| ReplayError::MigrationNotFound(id.clone()))?;
Self::apply_migration(&mut schema, migration)?;
}
Ok(schema)
}
pub fn replay_with_sources(
&self,
ordered_ids: &[String],
) -> Result<ReplaySources, ReplayError> {
let mut schema = Schema::default();
let mut last_per_ns = HashMap::new();
let mut entity_ns = HashMap::new();
for id in ordered_ids {
let migration = self
.graph
.get(id)
.ok_or_else(|| ReplayError::MigrationNotFound(id.clone()))?;
Self::apply_migration(&mut schema, migration)?;
let ns = namespace_of(id).to_string();
for entity in migration.get_entities() {
entity_ns.insert(entity, ns.clone());
}
last_per_ns.insert(ns, id.clone());
}
Ok(ReplaySources {
schema,
last_per_ns,
entity_ns,
})
}
pub fn apply_migration(schema: &mut Schema, migration: &Migration) -> Result<(), ReplayError> {
for (index, operation) in migration.operations.iter().enumerate() {
schema
.apply(operation)
.map_err(|inner| ReplayError::WithContext {
migration: migration.id.clone(),
op_num: index + 1,
inner: Box::new(inner),
})?;
}
Ok(())
}
}
pub fn namespace_of(id: &str) -> &str {
match id.rfind('/') {
Some(pos) => &id[..pos],
None => "",
}
}
pub fn compute_deps(
ops: &[Operation],
last_per_ns: &HashMap<String, String>,
entity_ns: &HashMap<(EntityKind, String), String>,
) -> Vec<String> {
let mut namespaces = BTreeSet::new();
namespaces.insert(String::new());
for op in ops {
for entity in op_entities(op) {
if let Some(ns) = entity_ns.get(&entity) {
namespaces.insert(ns.clone());
}
}
}
namespaces
.iter()
.filter_map(|ns| last_per_ns.get(ns).cloned())
.collect()
}
pub fn deterministic_name_from_ops(ops: &[Operation]) -> String {
let mut labels: Vec<&str> = Vec::new();
for op in ops {
if let Some(label) = op.entity_label()
&& !labels.contains(&label)
{
labels.push(label);
}
}
match labels.as_slice() {
[] => "changes".to_string(),
[a] => sanitize_id_part(a),
[a, b] => format!("{}_{}", sanitize_id_part(a), sanitize_id_part(b)),
[a, _, ..] => format!("{}_changes", sanitize_id_part(a)),
}
}
fn sanitize_id_part(value: &str) -> String {
let sanitized: String = value
.chars()
.map(|c| {
if c.is_ascii_lowercase() || c.is_ascii_digit() {
c
} else if c.is_ascii_alphabetic() {
c.to_ascii_lowercase()
} else {
'_'
}
})
.collect();
let trimmed = sanitized.trim_matches('_');
if trimmed.is_empty() {
"changes".to_string()
} else {
trimmed.to_string()
}
}
fn op_entities(op: &Operation) -> Vec<(EntityKind, String)> {
op.touched_entities()
}