use std::collections::{BTreeMap, BTreeSet};
use sha2::{Digest, Sha256};
use crate::{
DeclaredEntityVersion, EntityFragment, EntitySourceDigest, EntitySourceKey, FieldFragment,
FieldSourceKey, NamedTypeFragment, RelationFragment, SchemaContractError, SchemaMigrationPlan,
SchemaProposal, TypeSourceKey,
};
const ENTITY_SOURCE_DIGEST_PROFILE: &[u8] = b"icydb.entity-source-meaning.wire.v1";
impl SchemaProposal {
pub fn entity_source_digest(
&self,
source: &EntitySourceKey,
) -> Result<EntitySourceDigest, SchemaContractError> {
self.entity_source_digest_with_targets(source, &BTreeMap::new())
}
pub fn entity_source_digest_before_entity_renames(
&self,
source: &EntitySourceKey,
) -> Result<EntitySourceDigest, SchemaContractError> {
let targets = self
.migration()
.into_iter()
.flat_map(SchemaMigrationPlan::transitions)
.filter_map(|transition| {
transition
.from_name()
.map(|from| (transition.entity(), from))
})
.collect();
self.entity_source_digest_with_targets(source, &targets)
}
fn entity_source_digest_with_targets(
&self,
source: &EntitySourceKey,
target_names: &BTreeMap<&EntitySourceKey, &EntitySourceKey>,
) -> Result<EntitySourceDigest, SchemaContractError> {
let mut entities = BTreeMap::new();
let mut types = BTreeMap::new();
for fragment in self.fragments() {
for entity in fragment.entities() {
entities.insert(entity.source_key().clone(), entity);
}
for definition in fragment.types() {
types.insert(definition.source_key().clone(), definition);
}
}
let entity = entities
.get(source)
.copied()
.ok_or(SchemaContractError::InvalidMigrationReference)?;
let normalized = normalized_entity(entity, target_names)?;
let mut pending_types = Vec::new();
for field in entity.fields() {
field
.field_type()
.append_named_type_dependencies(&mut pending_types);
}
let mut relation_targets = relation_target_meanings(entity, &entities, &mut pending_types)?;
if !target_names.is_empty() {
for (target, _) in &mut relation_targets {
if let Some(predecessor) = target_names.get(target) {
target.clone_from(predecessor);
}
}
crate::compact_sort_unstable_by(&mut relation_targets, |left, right| {
left.0.cmp(&right.0)
});
if relation_targets
.windows(2)
.any(|pair| pair[0].0 == pair[1].0)
{
return Err(SchemaContractError::InvalidMigrationReference);
}
}
let reachable_types = reachable_type_meanings(&types, pending_types)?;
let encoded = crate::codec::encode_entity_source_meaning(
&normalized,
&relation_targets,
&reachable_types,
)?;
let mut hasher = Sha256::new();
hasher.update(ENTITY_SOURCE_DIGEST_PROFILE);
hasher.update(
u64::try_from(encoded.len())
.unwrap_or(u64::MAX)
.to_be_bytes(),
);
hasher.update(encoded);
Ok(EntitySourceDigest::from_bytes(hasher.finalize().into()))
}
}
fn normalized_entity(
entity: &EntityFragment,
target_names: &BTreeMap<&EntitySourceKey, &EntitySourceKey>,
) -> Result<EntityFragment, SchemaContractError> {
let relations = entity
.relations()
.iter()
.map(|relation| {
let Some(predecessor) = target_names.get(relation.target_entity()) else {
return Ok(relation.clone());
};
RelationFragment::try_new(
relation.name().clone(),
relation.source().clone(),
(*predecessor).clone(),
relation.target_fields().to_vec(),
relation.on_delete(),
)
})
.collect::<Result<Vec<_>, _>>()?;
EntityFragment::try_new(
entity.name().clone(),
DeclaredEntityVersion::try_new(1)?,
entity.fields().to_vec(),
entity.primary_key().to_vec(),
entity.indexes().to_vec(),
relations,
entity.constraints().to_vec(),
)
}
type RelationTargetMeaning = (EntitySourceKey, Vec<(FieldSourceKey, FieldFragment)>);
fn relation_target_meanings(
entity: &EntityFragment,
entities: &BTreeMap<EntitySourceKey, &EntityFragment>,
pending_types: &mut Vec<TypeSourceKey>,
) -> Result<Vec<RelationTargetMeaning>, SchemaContractError> {
let mut targets = BTreeMap::<EntitySourceKey, BTreeSet<FieldSourceKey>>::new();
for relation in entity.relations() {
targets
.entry(relation.target_entity().clone())
.or_default()
.extend(relation.target_fields().iter().cloned());
}
targets
.into_iter()
.map(|(target_source, field_sources)| {
let target = entities
.get(&target_source)
.copied()
.ok_or(SchemaContractError::InvalidMigrationReference)?;
let fields = field_sources
.into_iter()
.map(|field_source| {
let field = target
.fields()
.iter()
.find(|field| field.source_key() == &field_source)
.cloned()
.ok_or(SchemaContractError::InvalidMigrationReference)?;
field
.field_type()
.append_named_type_dependencies(pending_types);
Ok((field_source, field))
})
.collect::<Result<Vec<_>, SchemaContractError>>()?;
Ok((target_source, fields))
})
.collect()
}
fn reachable_type_meanings(
types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
mut pending: Vec<TypeSourceKey>,
) -> Result<Vec<NamedTypeFragment>, SchemaContractError> {
let mut reachable = BTreeSet::new();
while let Some(source) = pending.pop() {
if !reachable.insert(source.clone()) {
continue;
}
let definition = types
.get(&source)
.copied()
.ok_or(SchemaContractError::InvalidMigrationReference)?;
definition.append_named_type_dependencies(&mut pending);
}
reachable
.into_iter()
.map(|source| {
types
.get(&source)
.copied()
.cloned()
.ok_or(SchemaContractError::InvalidMigrationReference)
})
.collect()
}
#[cfg(test)]
mod tests;