venus_core/state/
schema.rs

1//! Schema evolution detection for Venus notebooks.
2//!
3//! Detects changes to struct definitions that may require cache invalidation.
4
5use std::collections::hash_map::DefaultHasher;
6use std::hash::{Hash, Hasher};
7
8/// A fingerprint of a type's schema for detecting breaking changes.
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct TypeFingerprint {
11    /// Type name (fully qualified)
12    pub type_name: String,
13
14    /// Hash of the type's structure
15    pub structure_hash: u64,
16
17    /// Field names in order (for structs)
18    pub fields: Vec<String>,
19
20    /// Field type names in order
21    pub field_types: Vec<String>,
22}
23
24impl TypeFingerprint {
25    /// Create a fingerprint from field information.
26    pub fn new(type_name: &str, fields: Vec<(String, String)>) -> Self {
27        let mut hasher = DefaultHasher::new();
28        type_name.hash(&mut hasher);
29        for (name, ty) in &fields {
30            name.hash(&mut hasher);
31            ty.hash(&mut hasher);
32        }
33
34        let (field_names, field_types): (Vec<_>, Vec<_>) = fields.into_iter().unzip();
35
36        Self {
37            type_name: type_name.to_string(),
38            structure_hash: hasher.finish(),
39            fields: field_names,
40            field_types,
41        }
42    }
43
44    /// Create a fingerprint for a primitive type.
45    pub fn primitive(type_name: &str) -> Self {
46        let mut hasher = DefaultHasher::new();
47        type_name.hash(&mut hasher);
48
49        Self {
50            type_name: type_name.to_string(),
51            structure_hash: hasher.finish(),
52            fields: Vec::new(),
53            field_types: Vec::new(),
54        }
55    }
56
57    /// Compare with another fingerprint and detect the type of change.
58    pub fn compare(&self, other: &TypeFingerprint) -> SchemaChange {
59        if self.type_name != other.type_name {
60            return SchemaChange::TypeRenamed {
61                old: self.type_name.clone(),
62                new: other.type_name.clone(),
63            };
64        }
65
66        if self.structure_hash == other.structure_hash {
67            return SchemaChange::None;
68        }
69
70        // Detect specific changes
71        let old_fields: std::collections::HashSet<_> = self.fields.iter().collect();
72        let new_fields: std::collections::HashSet<_> = other.fields.iter().collect();
73
74        let added: Vec<_> = new_fields
75            .difference(&old_fields)
76            .cloned()
77            .cloned()
78            .collect();
79        let removed: Vec<_> = old_fields
80            .difference(&new_fields)
81            .cloned()
82            .cloned()
83            .collect();
84
85        // Check for type changes in existing fields
86        let mut type_changes = Vec::new();
87        for (i, field) in self.fields.iter().enumerate() {
88            if let Some(new_idx) = other.fields.iter().position(|f| f == field)
89                && self.field_types.get(i) != other.field_types.get(new_idx)
90            {
91                type_changes.push((
92                    field.clone(),
93                    self.field_types.get(i).cloned().unwrap_or_default(),
94                    other.field_types.get(new_idx).cloned().unwrap_or_default(),
95                ));
96            }
97        }
98
99        // Determine if change is breaking
100        if !removed.is_empty() || !type_changes.is_empty() {
101            SchemaChange::Breaking {
102                added,
103                removed,
104                type_changes,
105            }
106        } else if !added.is_empty() {
107            SchemaChange::Additive { added }
108        } else {
109            // Some other structural change (e.g., field reordering)
110            SchemaChange::Breaking {
111                added: Vec::new(),
112                removed: Vec::new(),
113                type_changes: Vec::new(),
114            }
115        }
116    }
117}
118
119/// Describes a change between two schema versions.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub enum SchemaChange {
122    /// No change detected.
123    None,
124
125    /// Fields were added (non-breaking if optional).
126    Additive { added: Vec<String> },
127
128    /// Breaking change that requires cache invalidation.
129    Breaking {
130        added: Vec<String>,
131        removed: Vec<String>,
132        type_changes: Vec<(String, String, String)>, // (field, old_type, new_type)
133    },
134
135    /// Type was renamed (breaking).
136    TypeRenamed { old: String, new: String },
137}
138
139impl SchemaChange {
140    /// Check if this change is breaking (requires cache invalidation).
141    pub fn is_breaking(&self) -> bool {
142        matches!(
143            self,
144            SchemaChange::Breaking { .. } | SchemaChange::TypeRenamed { .. }
145        )
146    }
147
148    /// Get a human-readable description of the change.
149    pub fn description(&self) -> String {
150        match self {
151            SchemaChange::None => "No changes".to_string(),
152            SchemaChange::Additive { added } => {
153                format!("Added fields: {}", added.join(", "))
154            }
155            SchemaChange::Breaking {
156                added,
157                removed,
158                type_changes,
159            } => {
160                let mut parts = Vec::new();
161                if !added.is_empty() {
162                    parts.push(format!("added: {}", added.join(", ")));
163                }
164                if !removed.is_empty() {
165                    parts.push(format!("removed: {}", removed.join(", ")));
166                }
167                for (field, old, new) in type_changes {
168                    parts.push(format!("{}: {} -> {}", field, old, new));
169                }
170                format!("Breaking changes: {}", parts.join("; "))
171            }
172            SchemaChange::TypeRenamed { old, new } => {
173                format!("Type renamed: {} -> {}", old, new)
174            }
175        }
176    }
177}
178
179/// Extract type fingerprint from a syn ItemStruct.
180///
181/// **Planned feature**: Reserved for venus proc-macro crate to generate
182/// compile-time type fingerprints for schema evolution detection.
183/// This will enable automatic cache invalidation when struct definitions change.
184#[allow(dead_code)]
185pub fn fingerprint_from_struct(item: &syn::ItemStruct) -> TypeFingerprint {
186    let type_name = item.ident.to_string();
187
188    let fields: Vec<(String, String)> = match &item.fields {
189        syn::Fields::Named(named) => named
190            .named
191            .iter()
192            .map(|f| {
193                let name = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
194                let ty = quote::quote!(#f.ty).to_string();
195                (name, ty)
196            })
197            .collect(),
198        syn::Fields::Unnamed(unnamed) => unnamed
199            .unnamed
200            .iter()
201            .enumerate()
202            .map(|(i, f)| {
203                let ty = quote::quote!(#f.ty).to_string();
204                (format!("{}", i), ty)
205            })
206            .collect(),
207        syn::Fields::Unit => Vec::new(),
208    };
209
210    TypeFingerprint::new(&type_name, fields)
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_fingerprint_equality() {
219        let fp1 = TypeFingerprint::new(
220            "TestStruct",
221            vec![
222                ("x".to_string(), "i32".to_string()),
223                ("y".to_string(), "String".to_string()),
224            ],
225        );
226
227        let fp2 = TypeFingerprint::new(
228            "TestStruct",
229            vec![
230                ("x".to_string(), "i32".to_string()),
231                ("y".to_string(), "String".to_string()),
232            ],
233        );
234
235        assert_eq!(fp1, fp2);
236        assert_eq!(fp1.compare(&fp2), SchemaChange::None);
237    }
238
239    #[test]
240    fn test_additive_change() {
241        let fp1 = TypeFingerprint::new("TestStruct", vec![("x".to_string(), "i32".to_string())]);
242
243        let fp2 = TypeFingerprint::new(
244            "TestStruct",
245            vec![
246                ("x".to_string(), "i32".to_string()),
247                ("y".to_string(), "String".to_string()),
248            ],
249        );
250
251        let change = fp1.compare(&fp2);
252        assert!(!change.is_breaking());
253        match change {
254            SchemaChange::Additive { added } => {
255                assert_eq!(added, vec!["y".to_string()]);
256            }
257            _ => panic!("Expected Additive change"),
258        }
259    }
260
261    #[test]
262    fn test_breaking_removal() {
263        let fp1 = TypeFingerprint::new(
264            "TestStruct",
265            vec![
266                ("x".to_string(), "i32".to_string()),
267                ("y".to_string(), "String".to_string()),
268            ],
269        );
270
271        let fp2 = TypeFingerprint::new("TestStruct", vec![("x".to_string(), "i32".to_string())]);
272
273        let change = fp1.compare(&fp2);
274        assert!(change.is_breaking());
275    }
276
277    #[test]
278    fn test_breaking_type_change() {
279        let fp1 = TypeFingerprint::new("TestStruct", vec![("x".to_string(), "i32".to_string())]);
280
281        let fp2 = TypeFingerprint::new("TestStruct", vec![("x".to_string(), "i64".to_string())]);
282
283        let change = fp1.compare(&fp2);
284        assert!(change.is_breaking());
285        match change {
286            SchemaChange::Breaking { type_changes, .. } => {
287                assert_eq!(type_changes.len(), 1);
288                assert_eq!(type_changes[0].0, "x");
289            }
290            _ => panic!("Expected Breaking change"),
291        }
292    }
293
294    #[test]
295    fn test_type_renamed() {
296        let fp1 = TypeFingerprint::new("OldName", vec![]);
297        let fp2 = TypeFingerprint::new("NewName", vec![]);
298
299        let change = fp1.compare(&fp2);
300        assert!(change.is_breaking());
301        match change {
302            SchemaChange::TypeRenamed { old, new } => {
303                assert_eq!(old, "OldName");
304                assert_eq!(new, "NewName");
305            }
306            _ => panic!("Expected TypeRenamed"),
307        }
308    }
309
310    #[test]
311    fn test_primitive_fingerprint() {
312        let fp1 = TypeFingerprint::primitive("i32");
313        let fp2 = TypeFingerprint::primitive("i32");
314        let fp3 = TypeFingerprint::primitive("i64");
315
316        assert_eq!(fp1.compare(&fp2), SchemaChange::None);
317        assert!(fp1.compare(&fp3).is_breaking());
318    }
319
320    #[test]
321    fn test_fingerprint_from_syn() {
322        let code = "struct Point { x: f64, y: f64 }";
323        let item: syn::ItemStruct = syn::parse_str(code).unwrap();
324
325        let fp = fingerprint_from_struct(&item);
326        assert_eq!(fp.type_name, "Point");
327        assert_eq!(fp.fields, vec!["x", "y"]);
328    }
329}