1use std::collections::{BTreeMap, BTreeSet};
4
5use sha2::{Digest, Sha256};
6
7use crate::{
8 DeclaredEntityVersion, EntityFragment, EntitySourceDigest, EntitySourceKey, FieldFragment,
9 FieldSourceKey, FieldType, NamedTypeFragment, SchemaContractError, SchemaProposal,
10 TypeSourceKey,
11};
12
13const ENTITY_SOURCE_DIGEST_PROFILE: &[u8] = b"icydb.entity-source-meaning.v1";
14
15impl SchemaProposal {
16 pub fn entity_source_digest(
27 &self,
28 source: &EntitySourceKey,
29 ) -> Result<EntitySourceDigest, SchemaContractError> {
30 let mut entities = BTreeMap::new();
31 let mut types = BTreeMap::new();
32 for fragment in self.fragments() {
33 for entity in fragment.entities() {
34 entities.insert(entity.source_key().clone(), entity);
35 }
36 for definition in fragment.types() {
37 types.insert(definition.source_key().clone(), definition);
38 }
39 }
40 let entity = entities
41 .get(source)
42 .copied()
43 .ok_or(SchemaContractError::InvalidMigrationReference)?;
44 let normalized = normalized_entity(entity)?;
45
46 let mut pending_types = Vec::new();
47 for field in entity.fields() {
48 collect_field_type_sources(field.field_type(), &mut pending_types);
49 }
50 let relation_targets = relation_target_meanings(entity, &entities, &mut pending_types)?;
51 let reachable_types = reachable_type_meanings(&types, pending_types)?;
52 let encoded = crate::codec::encode_entity_source_meaning(
53 &normalized,
54 &relation_targets,
55 &reachable_types,
56 )?;
57
58 let mut hasher = Sha256::new();
59 hasher.update(ENTITY_SOURCE_DIGEST_PROFILE);
60 hasher.update(
61 u64::try_from(encoded.len())
62 .unwrap_or(u64::MAX)
63 .to_be_bytes(),
64 );
65 hasher.update(encoded);
66 Ok(EntitySourceDigest::from_bytes(hasher.finalize().into()))
67 }
68}
69
70fn normalized_entity(entity: &EntityFragment) -> Result<EntityFragment, SchemaContractError> {
71 EntityFragment::try_new(
72 entity.name().clone(),
73 DeclaredEntityVersion::try_new(1)?,
74 entity.fields().to_vec(),
75 entity.primary_key().to_vec(),
76 entity.indexes().to_vec(),
77 entity.relations().to_vec(),
78 entity.constraints().to_vec(),
79 )
80}
81
82type RelationTargetMeaning = (EntitySourceKey, Vec<(FieldSourceKey, FieldFragment)>);
83
84fn relation_target_meanings(
85 entity: &EntityFragment,
86 entities: &BTreeMap<EntitySourceKey, &EntityFragment>,
87 pending_types: &mut Vec<TypeSourceKey>,
88) -> Result<Vec<RelationTargetMeaning>, SchemaContractError> {
89 let mut targets = BTreeMap::<EntitySourceKey, BTreeSet<FieldSourceKey>>::new();
90 for relation in entity.relations() {
91 targets
92 .entry(relation.target_entity().clone())
93 .or_default()
94 .extend(relation.target_fields().iter().cloned());
95 }
96 targets
97 .into_iter()
98 .map(|(target_source, field_sources)| {
99 let target = entities
100 .get(&target_source)
101 .copied()
102 .ok_or(SchemaContractError::InvalidMigrationReference)?;
103 let fields = field_sources
104 .into_iter()
105 .map(|field_source| {
106 let field = target
107 .fields()
108 .iter()
109 .find(|field| field.source_key() == &field_source)
110 .cloned()
111 .ok_or(SchemaContractError::InvalidMigrationReference)?;
112 collect_field_type_sources(field.field_type(), pending_types);
113 Ok((field_source, field))
114 })
115 .collect::<Result<Vec<_>, SchemaContractError>>()?;
116 Ok((target_source, fields))
117 })
118 .collect()
119}
120
121fn reachable_type_meanings(
122 types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
123 mut pending: Vec<TypeSourceKey>,
124) -> Result<Vec<NamedTypeFragment>, SchemaContractError> {
125 let mut reachable = BTreeSet::new();
126 while let Some(source) = pending.pop() {
127 if !reachable.insert(source.clone()) {
128 continue;
129 }
130 let definition = types
131 .get(&source)
132 .copied()
133 .ok_or(SchemaContractError::InvalidMigrationReference)?;
134 collect_named_type_sources(definition, &mut pending);
135 }
136 reachable
137 .into_iter()
138 .map(|source| {
139 types
140 .get(&source)
141 .copied()
142 .cloned()
143 .ok_or(SchemaContractError::InvalidMigrationReference)
144 })
145 .collect()
146}
147
148fn collect_named_type_sources(definition: &NamedTypeFragment, pending: &mut Vec<TypeSourceKey>) {
149 match definition {
150 NamedTypeFragment::Record(record) => {
151 for field in record.fields() {
152 collect_field_type_sources(field.field_type(), pending);
153 }
154 }
155 NamedTypeFragment::Enum(r#enum) => {
156 for variant in r#enum.variants() {
157 if let Some(payload) = variant.payload() {
158 collect_field_type_sources(payload, pending);
159 }
160 }
161 }
162 NamedTypeFragment::Newtype { inner, .. }
163 | NamedTypeFragment::List { item: inner, .. }
164 | NamedTypeFragment::Set { item: inner, .. } => {
165 collect_field_type_sources(inner, pending);
166 }
167 NamedTypeFragment::Map { key, value, .. } => {
168 collect_field_type_sources(key, pending);
169 collect_field_type_sources(value, pending);
170 }
171 NamedTypeFragment::Tuple { members, .. } => {
172 for member in members {
173 collect_field_type_sources(member.field_type(), pending);
174 }
175 }
176 }
177}
178
179fn collect_field_type_sources(field_type: &FieldType, pending: &mut Vec<TypeSourceKey>) {
180 match field_type {
181 FieldType::List(inner) => collect_field_type_sources(inner, pending),
182 FieldType::Named(source) => pending.push(source.clone()),
183 FieldType::Scalar(_) => {}
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use crate::{
190 DeclaredEntityVersion, EntityFragment, EntitySourceKey, EntityStoreAssignment,
191 ExpectedAcceptedHead, FieldFragment, FieldInsertPolicy, FieldSourceKey, FieldType,
192 SchemaFragment, SchemaName, SchemaProposal, SchemaSubmissionKey, TargetDatabaseIdentity,
193 TargetStoreIdentity,
194 };
195
196 fn proposal(version: u32, field_name: &str) -> SchemaProposal {
197 let id = FieldFragment::new(
198 SchemaName::try_new(field_name).expect("field name should admit"),
199 FieldType::Scalar(crate::ScalarType::Nat64),
200 false,
201 FieldInsertPolicy::Required,
202 None,
203 );
204 let entity = EntityFragment::try_new(
205 SchemaName::try_new("User").expect("entity name should admit"),
206 DeclaredEntityVersion::try_new(version).expect("version should admit"),
207 vec![id],
208 vec![FieldSourceKey::try_new(field_name).expect("field key should admit")],
209 Vec::new(),
210 Vec::new(),
211 Vec::new(),
212 )
213 .expect("entity should admit");
214 SchemaProposal::try_compose(
215 Vec::new(),
216 TargetDatabaseIdentity::from_bytes([1; 32]),
217 SchemaSubmissionKey::try_new("source-digest").expect("submission should admit"),
218 ExpectedAcceptedHead::Empty,
219 vec![SchemaFragment::try_new(vec![entity], Vec::new()).expect("fragment should admit")],
220 vec![EntityStoreAssignment::new(
221 EntitySourceKey::try_new("User").expect("entity key should admit"),
222 TargetStoreIdentity::from_bytes([2; 32]),
223 )],
224 Vec::new(),
225 None,
226 )
227 .expect("proposal should admit")
228 }
229
230 #[test]
231 fn source_digest_ignores_declared_version_but_not_entity_meaning() {
232 let source = EntitySourceKey::try_new("User").expect("source should admit");
233 assert_eq!(
234 proposal(1, "id")
235 .entity_source_digest(&source)
236 .expect("digest should derive"),
237 proposal(7, "id")
238 .entity_source_digest(&source)
239 .expect("digest should derive"),
240 );
241 assert_ne!(
242 proposal(1, "id")
243 .entity_source_digest(&source)
244 .expect("digest should derive"),
245 proposal(1, "other")
246 .entity_source_digest(&source)
247 .expect("digest should derive"),
248 );
249 }
250}