schema-sync 1.0.0

Production-grade schema synchronization for multi-tenant databases
Documentation
//! Developer: s4gor
//! Github: https://github.com/s4gor
//!
//! Schema diff calculation and representation
//!
//! This module provides types and logic for calculating differences
//! between two schema snapshots. The diff is structured to support
//! both human-readable and machine-readable output formats.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// A complete diff between two schema snapshots
///
/// This represents all changes needed to transform schema A into schema B.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SchemaDiff {
    /// Tables that were added
    pub tables_added: Vec<String>,

    /// Tables that were removed
    pub tables_removed: Vec<String>,

    /// Tables that were modified
    pub tables_modified: HashMap<String, TableDiff>,

    /// Views that were added
    pub views_added: Vec<String>,

    /// Views that were removed
    pub views_removed: Vec<String>,

    /// Views that were modified
    pub views_modified: HashMap<String, ViewDiff>,

    /// Functions that were added
    pub functions_added: Vec<String>,

    /// Functions that were removed
    pub functions_removed: Vec<String>,

    /// Functions that were modified
    pub functions_modified: HashMap<String, FunctionDiff>,

    /// Types that were added
    pub types_added: Vec<String>,

    /// Types that were removed
    pub types_removed: Vec<String>,

    /// Types that were modified
    pub types_modified: HashMap<String, TypeDiff>,
}

impl SchemaDiff {
    /// Check if this diff is empty (no changes)
    pub fn is_empty(&self) -> bool {
        self.tables_added.is_empty()
            && self.tables_removed.is_empty()
            && self.tables_modified.is_empty()
            && self.views_added.is_empty()
            && self.views_removed.is_empty()
            && self.views_modified.is_empty()
            && self.functions_added.is_empty()
            && self.functions_removed.is_empty()
            && self.functions_modified.is_empty()
            && self.types_added.is_empty()
            && self.types_removed.is_empty()
            && self.types_modified.is_empty()
    }

    /// Count total number of changes
    pub fn change_count(&self) -> usize {
        self.tables_added.len()
            + self.tables_removed.len()
            + self.tables_modified.len()
            + self.views_added.len()
            + self.views_removed.len()
            + self.views_modified.len()
            + self.functions_added.len()
            + self.functions_removed.len()
            + self.functions_modified.len()
            + self.types_added.len()
            + self.types_removed.len()
            + self.types_modified.len()
    }
}

/// Diff for a single table
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TableDiff {
    /// Columns that were added
    pub columns_added: Vec<String>,

    /// Columns that were removed
    pub columns_removed: Vec<String>,

    /// Columns that were modified
    pub columns_modified: HashMap<String, ColumnDiff>,

    /// Primary key changes
    pub primary_key_changed: bool,

    /// Foreign keys that were added
    pub foreign_keys_added: Vec<String>,

    /// Foreign keys that were removed
    pub foreign_keys_removed: Vec<String>,

    /// Unique constraints that were added
    pub unique_constraints_added: Vec<String>,

    /// Unique constraints that were removed
    pub unique_constraints_removed: Vec<String>,

    /// Indexes that were added
    pub indexes_added: Vec<String>,

    /// Indexes that were removed
    pub indexes_removed: Vec<String>,

    /// Check constraints that were added
    pub check_constraints_added: Vec<String>,

    /// Check constraints that were removed
    pub check_constraints_removed: Vec<String>,
}

/// Diff for a single column
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ColumnDiff {
    /// Whether the data type changed
    pub data_type_changed: bool,

    /// Old data type (if changed)
    pub old_data_type: Option<String>,

    /// New data type (if changed)
    pub new_data_type: Option<String>,

    /// Whether nullability changed
    pub nullability_changed: bool,

    /// Whether default value changed
    pub default_changed: bool,

    /// Old default value (if changed)
    pub old_default: Option<String>,

    /// New default value (if changed)
    pub new_default: Option<String>,
}

/// Diff for a view
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ViewDiff {
    /// Whether the definition changed
    pub definition_changed: bool,
}

/// Diff for a function
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionDiff {
    /// Whether the signature changed
    pub signature_changed: bool,

    /// Whether the body changed
    pub body_changed: bool,

    /// Whether the return type changed
    pub return_type_changed: bool,
}

/// Diff for a type
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TypeDiff {
    /// Whether the definition changed
    pub definition_changed: bool,
}

/// Trait for calculating diffs between snapshots
///
/// This abstraction allows different diff algorithms to be plugged in,
/// potentially supporting different strategies (e.g., three-way merge,
/// conflict detection, etc.)
pub trait DiffCalculator: Send + Sync {
    /// Calculate the diff between two snapshots
    fn calculate_diff(&self, from: &crate::snapshot::SchemaSnapshot, to: &crate::snapshot::SchemaSnapshot) -> SchemaDiff;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_empty_diff() {
        let diff = SchemaDiff {
            tables_added: vec![],
            tables_removed: vec![],
            tables_modified: HashMap::new(),
            views_added: vec![],
            views_removed: vec![],
            views_modified: HashMap::new(),
            functions_added: vec![],
            functions_removed: vec![],
            functions_modified: HashMap::new(),
            types_added: vec![],
            types_removed: vec![],
            types_modified: HashMap::new(),
        };
        assert!(diff.is_empty());
        assert_eq!(diff.change_count(), 0);
    }

    #[test]
    fn test_diff_with_changes() {
        let mut diff = SchemaDiff {
            tables_added: vec!["users".to_string()],
            tables_removed: vec!["old_table".to_string()],
            tables_modified: HashMap::new(),
            views_added: vec![],
            views_removed: vec![],
            views_modified: HashMap::new(),
            functions_added: vec![],
            functions_removed: vec![],
            functions_modified: HashMap::new(),
            types_added: vec![],
            types_removed: vec![],
            types_modified: HashMap::new(),
        };
        assert!(!diff.is_empty());
        assert_eq!(diff.change_count(), 2);

        let mut table_diff = TableDiff {
            columns_added: vec!["email".to_string()],
            columns_removed: vec![],
            columns_modified: HashMap::new(),
            primary_key_changed: false,
            foreign_keys_added: vec![],
            foreign_keys_removed: vec![],
            unique_constraints_added: vec![],
            unique_constraints_removed: vec![],
            indexes_added: vec![],
            indexes_removed: vec![],
            check_constraints_added: vec![],
            check_constraints_removed: vec![],
        };
        diff.tables_modified.insert("posts".to_string(), table_diff);
        assert_eq!(diff.change_count(), 3);
    }

    #[test]
    fn test_diff_serialization() {
        let diff = SchemaDiff {
            tables_added: vec!["users".to_string()],
            tables_removed: vec![],
            tables_modified: HashMap::new(),
            views_added: vec![],
            views_removed: vec![],
            views_modified: HashMap::new(),
            functions_added: vec![],
            functions_removed: vec![],
            functions_modified: HashMap::new(),
            types_added: vec![],
            types_removed: vec![],
            types_modified: HashMap::new(),
        };

        let json = serde_json::to_string(&diff).unwrap();
        let deserialized: SchemaDiff = serde_json::from_str(&json).unwrap();
        assert_eq!(diff, deserialized);
    }
}