Skip to main content

cufflink_types/manifest/
schema_diff.rs

1use serde::{Deserialize, Serialize};
2
3/// Description of the changes a deploy is about to apply, passed as the
4/// payload to the `on_migrate` hook so user code can decide which backfill
5/// blocks should fire.
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
7pub struct SchemaDiff {
8    #[serde(default)]
9    pub added_tables: Vec<String>,
10    #[serde(default)]
11    pub dropped_tables: Vec<String>,
12    /// (table, column) pairs for newly added columns. In phase 2 these
13    /// already exist in the database (added in phase 1) but may be NULL on
14    /// existing rows; the hook is the place to backfill them.
15    #[serde(default)]
16    pub added_columns: Vec<(String, String)>,
17    /// (table, column) pairs for columns that will be dropped in phase 3.
18    /// During phase 2 these are still present in the database, so the hook
19    /// can read them to populate new columns before they disappear.
20    #[serde(default)]
21    pub dropped_columns: Vec<(String, String)>,
22    /// (table, column, old_type, new_type)
23    #[serde(default)]
24    pub type_changes: Vec<(String, String, String, String)>,
25    /// (table, column, new_nullable)
26    #[serde(default)]
27    pub nullability_changes: Vec<(String, String, bool)>,
28}
29
30impl SchemaDiff {
31    pub fn added_column(&self, table: &str, col: &str) -> bool {
32        self.added_columns
33            .iter()
34            .any(|(t, c)| t == table && c == col)
35    }
36
37    pub fn dropped_column(&self, table: &str, col: &str) -> bool {
38        self.dropped_columns
39            .iter()
40            .any(|(t, c)| t == table && c == col)
41    }
42
43    pub fn added_table(&self, table: &str) -> bool {
44        self.added_tables.iter().any(|t| t == table)
45    }
46
47    pub fn dropped_table(&self, table: &str) -> bool {
48        self.dropped_tables.iter().any(|t| t == table)
49    }
50
51    pub fn is_empty(&self) -> bool {
52        self.added_tables.is_empty()
53            && self.dropped_tables.is_empty()
54            && self.added_columns.is_empty()
55            && self.dropped_columns.is_empty()
56            && self.type_changes.is_empty()
57            && self.nullability_changes.is_empty()
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn test_schema_diff_helpers() {
67        let diff = SchemaDiff {
68            added_tables: vec!["batches".into()],
69            dropped_tables: vec!["legacy_batches".into()],
70            added_columns: vec![("pickups".into(), "min".into())],
71            dropped_columns: vec![("pickups".into(), "midpoint".into())],
72            type_changes: vec![],
73            nullability_changes: vec![],
74        };
75        assert!(!diff.is_empty());
76        assert!(diff.added_table("batches"));
77        assert!(!diff.added_table("nope"));
78        assert!(diff.dropped_table("legacy_batches"));
79        assert!(diff.added_column("pickups", "min"));
80        assert!(!diff.added_column("pickups", "max"));
81        assert!(diff.dropped_column("pickups", "midpoint"));
82    }
83
84    #[test]
85    fn test_schema_diff_is_empty_default() {
86        assert!(SchemaDiff::default().is_empty());
87    }
88
89    #[test]
90    fn test_schema_diff_serialization_roundtrip() {
91        let diff = SchemaDiff {
92            added_tables: vec![],
93            dropped_tables: vec![],
94            added_columns: vec![("t".into(), "c".into())],
95            dropped_columns: vec![],
96            type_changes: vec![("t".into(), "c".into(), "TEXT".into(), "INTEGER".into())],
97            nullability_changes: vec![("t".into(), "c".into(), false)],
98        };
99        let json = serde_json::to_string(&diff).unwrap();
100        let parsed: SchemaDiff = serde_json::from_str(&json).unwrap();
101        assert!(parsed.added_column("t", "c"));
102        assert_eq!(parsed.type_changes.len(), 1);
103        assert_eq!(parsed.nullability_changes.len(), 1);
104    }
105}