1use super::migration::MigrationRegistry;
7use super::version::ChangeKind;
8use crate::traits::TypeInfo;
9use std::sync::Arc;
10
11#[derive(Debug, Clone)]
16pub struct CompatibilityReport {
17 pub compatible: bool,
19 pub changes: Vec<SchemaChange>,
21 pub migration_required: bool,
23 pub migration_available: bool,
25 pub summary: String,
27}
28
29impl CompatibilityReport {
30 pub fn compatible() -> Self {
32 Self {
33 compatible: true,
34 changes: Vec::new(),
35 migration_required: false,
36 migration_available: true,
37 summary: "Schemas are compatible".to_string(),
38 }
39 }
40
41 pub fn incompatible(changes: Vec<SchemaChange>, migration_available: bool) -> Self {
43 let breaking_count = changes.iter().filter(|c| c.kind.is_breaking()).count();
44 let summary = if migration_available {
45 format!("{} breaking changes, migration available", breaking_count)
46 } else {
47 format!(
48 "{} breaking changes, no migration available",
49 breaking_count
50 )
51 };
52
53 Self {
54 compatible: false,
55 changes,
56 migration_required: true,
57 migration_available,
58 summary,
59 }
60 }
61
62 pub fn has_breaking_changes(&self) -> bool {
64 self.changes.iter().any(|c| c.kind.is_breaking())
65 }
66
67 pub fn breaking_changes(&self) -> Vec<&SchemaChange> {
69 self.changes
70 .iter()
71 .filter(|c| c.kind.is_breaking())
72 .collect()
73 }
74
75 pub fn non_breaking_changes(&self) -> Vec<&SchemaChange> {
77 self.changes
78 .iter()
79 .filter(|c| !c.kind.is_breaking())
80 .collect()
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct SchemaChange {
87 pub kind: ChangeKind,
89 pub field: String,
91 pub old_type: Option<String>,
93 pub new_type: Option<String>,
95 pub description: String,
97}
98
99impl SchemaChange {
100 pub fn new(kind: ChangeKind, field: impl Into<String>) -> Self {
102 let field = field.into();
103 Self {
104 description: format!("{}: {}", kind.description(), field),
105 kind,
106 field,
107 old_type: None,
108 new_type: None,
109 }
110 }
111
112 pub fn with_old_type(mut self, old_type: impl Into<String>) -> Self {
114 self.old_type = Some(old_type.into());
115 self
116 }
117
118 pub fn with_new_type(mut self, new_type: impl Into<String>) -> Self {
120 self.new_type = Some(new_type.into());
121 self
122 }
123
124 pub fn with_description(mut self, description: impl Into<String>) -> Self {
126 self.description = description.into();
127 self
128 }
129}
130
131impl std::fmt::Display for SchemaChange {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 write!(f, "{}", self.description)?;
134 if let (Some(old), Some(new)) = (&self.old_type, &self.new_type) {
135 write!(f, " ({} -> {})", old, new)?;
136 }
137 Ok(())
138 }
139}
140
141pub struct CompatibilityMatrix {
146 migrations: Arc<MigrationRegistry>,
148}
149
150impl CompatibilityMatrix {
151 pub fn new(migrations: Arc<MigrationRegistry>) -> Self {
153 Self { migrations }
154 }
155
156 pub fn check(&self, from: &TypeInfo, to: &TypeInfo) -> CompatibilityReport {
158 if from.hash != 0 && from.hash == to.hash {
160 return CompatibilityReport::compatible();
161 }
162
163 let changes = self.detect_changes(from, to);
165
166 if changes.is_empty() {
167 return CompatibilityReport::compatible();
168 }
169
170 let has_breaking = changes.iter().any(|c| c.kind.is_breaking());
172
173 if !has_breaking {
174 return CompatibilityReport {
176 compatible: true,
177 changes,
178 migration_required: false,
179 migration_available: true,
180 summary: "Compatible with non-breaking changes".to_string(),
181 };
182 }
183
184 let migration_available = self.can_migrate(from.hash, to.hash);
186
187 CompatibilityReport::incompatible(changes, migration_available)
188 }
189
190 pub fn detect_changes(&self, from: &TypeInfo, to: &TypeInfo) -> Vec<SchemaChange> {
192 let mut changes = Vec::new();
193
194 let from_fields: std::collections::HashMap<&str, _> =
196 from.fields.iter().map(|f| (f.name.as_str(), f)).collect();
197
198 let to_fields: std::collections::HashMap<&str, _> =
200 to.fields.iter().map(|f| (f.name.as_str(), f)).collect();
201
202 for (name, from_field) in &from_fields {
204 if !to_fields.contains_key(name) {
205 changes.push(
206 SchemaChange::new(ChangeKind::RemoveField, *name)
207 .with_old_type(&from_field.type_name),
208 );
209 }
210 }
211
212 for (name, to_field) in &to_fields {
214 match from_fields.get(name) {
215 None => {
216 let kind = if to_field.optional {
218 ChangeKind::AddOptionalField
219 } else {
220 ChangeKind::AddRequiredField
221 };
222 changes.push(SchemaChange::new(kind, *name).with_new_type(&to_field.type_name));
223 }
224 Some(from_field) => {
225 if from_field.type_name != to_field.type_name {
227 changes.push(
228 SchemaChange::new(ChangeKind::ChangeFieldType, *name)
229 .with_old_type(&from_field.type_name)
230 .with_new_type(&to_field.type_name),
231 );
232 }
233
234 if from_field.optional && !to_field.optional {
236 changes.push(SchemaChange::new(ChangeKind::MakeRequired, *name));
237 }
238
239 if !from_field.optional && to_field.optional {
241 changes.push(SchemaChange::new(ChangeKind::MakeOptional, *name));
242 }
243 }
244 }
245 }
246
247 changes
248 }
249
250 pub fn can_migrate(&self, from_hash: u64, to_hash: u64) -> bool {
252 self.migrations.has_path(from_hash, to_hash)
253 }
254
255 pub fn detect_potential_renames(
260 &self,
261 from: &TypeInfo,
262 to: &TypeInfo,
263 ) -> Vec<(String, String)> {
264 let mut potential_renames = Vec::new();
265
266 let removed: Vec<_> = from
268 .fields
269 .iter()
270 .filter(|f| !to.fields.iter().any(|t| t.name == f.name))
271 .collect();
272
273 let added: Vec<_> = to
275 .fields
276 .iter()
277 .filter(|f| !from.fields.iter().any(|t| t.name == f.name))
278 .collect();
279
280 for removed_field in &removed {
282 for added_field in &added {
283 if removed_field.type_name == added_field.type_name
284 && removed_field.optional == added_field.optional
285 {
286 potential_renames.push((removed_field.name.clone(), added_field.name.clone()));
287 }
288 }
289 }
290
291 potential_renames
292 }
293}
294
295impl Default for CompatibilityMatrix {
296 fn default() -> Self {
297 Self::new(Arc::new(MigrationRegistry::new()))
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use crate::traits::FieldInfo;
305
306 fn create_v1_schema() -> TypeInfo {
307 TypeInfo::new("Order", 1).with_hash(100).with_fields(vec![
308 FieldInfo::new("id", "String"),
309 FieldInfo::new("amount", "f64"),
310 FieldInfo::new("status", "String"),
311 ])
312 }
313
314 fn create_v2_schema_compatible() -> TypeInfo {
315 TypeInfo::new("Order", 2).with_hash(200).with_fields(vec![
316 FieldInfo::new("id", "String"),
317 FieldInfo::new("amount", "f64"),
318 FieldInfo::new("status", "String"),
319 FieldInfo::new("notes", "String").optional(),
320 ])
321 }
322
323 fn create_v2_schema_breaking() -> TypeInfo {
324 TypeInfo::new("Order", 2).with_hash(200).with_fields(vec![
325 FieldInfo::new("id", "String"),
326 FieldInfo::new("total", "f64"), FieldInfo::new("status", "i32"), ])
329 }
330
331 #[test]
332 fn identical_schemas() {
333 let matrix = CompatibilityMatrix::default();
334 let v1 = create_v1_schema();
335
336 let report = matrix.check(&v1, &v1);
337 assert!(report.compatible);
338 assert!(report.changes.is_empty());
339 }
340
341 #[test]
342 fn compatible_with_optional_field() {
343 let matrix = CompatibilityMatrix::default();
344 let v1 = create_v1_schema();
345 let v2 = create_v2_schema_compatible();
346
347 let report = matrix.check(&v1, &v2);
348 assert!(report.compatible);
349 assert!(!report.migration_required);
350 assert_eq!(report.changes.len(), 1);
351 assert_eq!(report.changes[0].kind, ChangeKind::AddOptionalField);
352 }
353
354 #[test]
355 fn breaking_changes() {
356 let matrix = CompatibilityMatrix::default();
357 let v1 = create_v1_schema();
358 let v2 = create_v2_schema_breaking();
359
360 let report = matrix.check(&v1, &v2);
361 assert!(!report.compatible);
362 assert!(report.migration_required);
363 assert!(report.has_breaking_changes());
364
365 let breaking = report.breaking_changes();
367 assert!(!breaking.is_empty());
368 }
369
370 #[test]
371 fn detect_changes_removed_field() {
372 let matrix = CompatibilityMatrix::default();
373
374 let v1 = TypeInfo::new("Test", 1)
375 .with_fields(vec![FieldInfo::new("a", "i32"), FieldInfo::new("b", "i32")]);
376
377 let v2 = TypeInfo::new("Test", 2).with_fields(vec![FieldInfo::new("a", "i32")]);
378
379 let changes = matrix.detect_changes(&v1, &v2);
380 assert_eq!(changes.len(), 1);
381 assert_eq!(changes[0].kind, ChangeKind::RemoveField);
382 assert_eq!(changes[0].field, "b");
383 }
384
385 #[test]
386 fn detect_changes_type_change() {
387 let matrix = CompatibilityMatrix::default();
388
389 let v1 = TypeInfo::new("Test", 1).with_fields(vec![FieldInfo::new("value", "i32")]);
390
391 let v2 = TypeInfo::new("Test", 2).with_fields(vec![FieldInfo::new("value", "f64")]);
392
393 let changes = matrix.detect_changes(&v1, &v2);
394 assert_eq!(changes.len(), 1);
395 assert_eq!(changes[0].kind, ChangeKind::ChangeFieldType);
396 assert_eq!(changes[0].old_type, Some("i32".to_string()));
397 assert_eq!(changes[0].new_type, Some("f64".to_string()));
398 }
399
400 #[test]
401 fn detect_potential_renames() {
402 let matrix = CompatibilityMatrix::default();
403
404 let v1 = TypeInfo::new("Test", 1).with_fields(vec![
405 FieldInfo::new("old_name", "String"),
406 FieldInfo::new("other", "i32"),
407 ]);
408
409 let v2 = TypeInfo::new("Test", 2).with_fields(vec![
410 FieldInfo::new("new_name", "String"),
411 FieldInfo::new("other", "i32"),
412 ]);
413
414 let renames = matrix.detect_potential_renames(&v1, &v2);
415 assert_eq!(renames.len(), 1);
416 assert_eq!(renames[0], ("old_name".to_string(), "new_name".to_string()));
417 }
418
419 #[test]
420 fn with_migration_available() {
421 let migrations = Arc::new(MigrationRegistry::new());
422
423 use super::super::migration::Migration;
425 migrations
426 .register(Migration::new(
427 "Order@v1",
428 100,
429 "Order@v2",
430 200,
431 |_arena, offset| Ok(offset),
432 ))
433 .unwrap();
434
435 let matrix = CompatibilityMatrix::new(migrations);
436 let v1 = create_v1_schema();
437 let v2 = create_v2_schema_breaking();
438
439 let report = matrix.check(&v1, &v2);
440 assert!(!report.compatible);
441 assert!(report.migration_available);
442 }
443}