Skip to main content

schema_sync/
diff.rs

1//! Developer: s4gor
2//! Github: https://github.com/s4gor
3//!
4//! Schema diff calculation and representation
5//!
6//! This module provides types and logic for calculating differences
7//! between two schema snapshots. The diff is structured to support
8//! both human-readable and machine-readable output formats.
9
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13/// A complete diff between two schema snapshots
14///
15/// This represents all changes needed to transform schema A into schema B.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct SchemaDiff {
18    /// Tables that were added
19    pub tables_added: Vec<String>,
20
21    /// Tables that were removed
22    pub tables_removed: Vec<String>,
23
24    /// Tables that were modified
25    pub tables_modified: HashMap<String, TableDiff>,
26
27    /// Views that were added
28    pub views_added: Vec<String>,
29
30    /// Views that were removed
31    pub views_removed: Vec<String>,
32
33    /// Views that were modified
34    pub views_modified: HashMap<String, ViewDiff>,
35
36    /// Functions that were added
37    pub functions_added: Vec<String>,
38
39    /// Functions that were removed
40    pub functions_removed: Vec<String>,
41
42    /// Functions that were modified
43    pub functions_modified: HashMap<String, FunctionDiff>,
44
45    /// Types that were added
46    pub types_added: Vec<String>,
47
48    /// Types that were removed
49    pub types_removed: Vec<String>,
50
51    /// Types that were modified
52    pub types_modified: HashMap<String, TypeDiff>,
53}
54
55impl SchemaDiff {
56    /// Check if this diff is empty (no changes)
57    pub fn is_empty(&self) -> bool {
58        self.tables_added.is_empty()
59            && self.tables_removed.is_empty()
60            && self.tables_modified.is_empty()
61            && self.views_added.is_empty()
62            && self.views_removed.is_empty()
63            && self.views_modified.is_empty()
64            && self.functions_added.is_empty()
65            && self.functions_removed.is_empty()
66            && self.functions_modified.is_empty()
67            && self.types_added.is_empty()
68            && self.types_removed.is_empty()
69            && self.types_modified.is_empty()
70    }
71
72    /// Count total number of changes
73    pub fn change_count(&self) -> usize {
74        self.tables_added.len()
75            + self.tables_removed.len()
76            + self.tables_modified.len()
77            + self.views_added.len()
78            + self.views_removed.len()
79            + self.views_modified.len()
80            + self.functions_added.len()
81            + self.functions_removed.len()
82            + self.functions_modified.len()
83            + self.types_added.len()
84            + self.types_removed.len()
85            + self.types_modified.len()
86    }
87}
88
89/// Diff for a single table
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct TableDiff {
92    /// Columns that were added
93    pub columns_added: Vec<String>,
94
95    /// Columns that were removed
96    pub columns_removed: Vec<String>,
97
98    /// Columns that were modified
99    pub columns_modified: HashMap<String, ColumnDiff>,
100
101    /// Primary key changes
102    pub primary_key_changed: bool,
103
104    /// Foreign keys that were added
105    pub foreign_keys_added: Vec<String>,
106
107    /// Foreign keys that were removed
108    pub foreign_keys_removed: Vec<String>,
109
110    /// Unique constraints that were added
111    pub unique_constraints_added: Vec<String>,
112
113    /// Unique constraints that were removed
114    pub unique_constraints_removed: Vec<String>,
115
116    /// Indexes that were added
117    pub indexes_added: Vec<String>,
118
119    /// Indexes that were removed
120    pub indexes_removed: Vec<String>,
121
122    /// Check constraints that were added
123    pub check_constraints_added: Vec<String>,
124
125    /// Check constraints that were removed
126    pub check_constraints_removed: Vec<String>,
127}
128
129/// Diff for a single column
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub struct ColumnDiff {
132    /// Whether the data type changed
133    pub data_type_changed: bool,
134
135    /// Old data type (if changed)
136    pub old_data_type: Option<String>,
137
138    /// New data type (if changed)
139    pub new_data_type: Option<String>,
140
141    /// Whether nullability changed
142    pub nullability_changed: bool,
143
144    /// Whether default value changed
145    pub default_changed: bool,
146
147    /// Old default value (if changed)
148    pub old_default: Option<String>,
149
150    /// New default value (if changed)
151    pub new_default: Option<String>,
152}
153
154/// Diff for a view
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct ViewDiff {
157    /// Whether the definition changed
158    pub definition_changed: bool,
159}
160
161/// Diff for a function
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163pub struct FunctionDiff {
164    /// Whether the signature changed
165    pub signature_changed: bool,
166
167    /// Whether the body changed
168    pub body_changed: bool,
169
170    /// Whether the return type changed
171    pub return_type_changed: bool,
172}
173
174/// Diff for a type
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176pub struct TypeDiff {
177    /// Whether the definition changed
178    pub definition_changed: bool,
179}
180
181/// Trait for calculating diffs between snapshots
182///
183/// This abstraction allows different diff algorithms to be plugged in,
184/// potentially supporting different strategies (e.g., three-way merge,
185/// conflict detection, etc.)
186pub trait DiffCalculator: Send + Sync {
187    /// Calculate the diff between two snapshots
188    fn calculate_diff(&self, from: &crate::snapshot::SchemaSnapshot, to: &crate::snapshot::SchemaSnapshot) -> SchemaDiff;
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_empty_diff() {
197        let diff = SchemaDiff {
198            tables_added: vec![],
199            tables_removed: vec![],
200            tables_modified: HashMap::new(),
201            views_added: vec![],
202            views_removed: vec![],
203            views_modified: HashMap::new(),
204            functions_added: vec![],
205            functions_removed: vec![],
206            functions_modified: HashMap::new(),
207            types_added: vec![],
208            types_removed: vec![],
209            types_modified: HashMap::new(),
210        };
211        assert!(diff.is_empty());
212        assert_eq!(diff.change_count(), 0);
213    }
214
215    #[test]
216    fn test_diff_with_changes() {
217        let mut diff = SchemaDiff {
218            tables_added: vec!["users".to_string()],
219            tables_removed: vec!["old_table".to_string()],
220            tables_modified: HashMap::new(),
221            views_added: vec![],
222            views_removed: vec![],
223            views_modified: HashMap::new(),
224            functions_added: vec![],
225            functions_removed: vec![],
226            functions_modified: HashMap::new(),
227            types_added: vec![],
228            types_removed: vec![],
229            types_modified: HashMap::new(),
230        };
231        assert!(!diff.is_empty());
232        assert_eq!(diff.change_count(), 2);
233
234        let mut table_diff = TableDiff {
235            columns_added: vec!["email".to_string()],
236            columns_removed: vec![],
237            columns_modified: HashMap::new(),
238            primary_key_changed: false,
239            foreign_keys_added: vec![],
240            foreign_keys_removed: vec![],
241            unique_constraints_added: vec![],
242            unique_constraints_removed: vec![],
243            indexes_added: vec![],
244            indexes_removed: vec![],
245            check_constraints_added: vec![],
246            check_constraints_removed: vec![],
247        };
248        diff.tables_modified.insert("posts".to_string(), table_diff);
249        assert_eq!(diff.change_count(), 3);
250    }
251
252    #[test]
253    fn test_diff_serialization() {
254        let diff = SchemaDiff {
255            tables_added: vec!["users".to_string()],
256            tables_removed: vec![],
257            tables_modified: HashMap::new(),
258            views_added: vec![],
259            views_removed: vec![],
260            views_modified: HashMap::new(),
261            functions_added: vec![],
262            functions_removed: vec![],
263            functions_modified: HashMap::new(),
264            types_added: vec![],
265            types_removed: vec![],
266            types_modified: HashMap::new(),
267        };
268
269        let json = serde_json::to_string(&diff).unwrap();
270        let deserialized: SchemaDiff = serde_json::from_str(&json).unwrap();
271        assert_eq!(diff, deserialized);
272    }
273}