schema-sync 1.0.0

Production-grade schema synchronization for multi-tenant databases
Documentation
//! Developer: s4gor
//! Github: https://github.com/s4gor
//!
//! Schema snapshot system
//!
//! Snapshots are normalized, deterministic representations of a schema
//! at a point in time. They enable:
//! - Diffing schema version A vs B
//! - Storing expected schema state
//! - Version control integration
//! - Deterministic hash-based versioning

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

use crate::errors::Result;

/// A normalized snapshot of a database schema
///
/// This structure is database-agnostic and represents the logical
/// structure of a schema, not the database-specific SQL.
///
/// Snapshots are deterministic: the same schema always produces
/// the same snapshot (order-independent where possible).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SchemaSnapshot {
    /// Unique identifier for this snapshot (deterministic hash)
    pub version_hash: String,

    /// Timestamp when snapshot was created (ISO 8601 string)
    pub created_at: String,

    /// Tables in this schema
    pub tables: HashMap<String, TableSnapshot>,

    /// Views in this schema
    pub views: HashMap<String, ViewSnapshot>,

    /// Functions/procedures in this schema
    pub functions: HashMap<String, FunctionSnapshot>,

    /// Extensions/enums/types in this schema
    pub types: HashMap<String, TypeSnapshot>,
}

/// Snapshot of a single table
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TableSnapshot {
    /// Table name
    pub name: String,

    /// Columns in this table
    pub columns: Vec<ColumnSnapshot>,

    /// Primary key constraint
    pub primary_key: Option<PrimaryKeySnapshot>,

    /// Foreign key constraints
    pub foreign_keys: Vec<ForeignKeySnapshot>,

    /// Unique constraints
    pub unique_constraints: Vec<UniqueConstraintSnapshot>,

    /// Indexes on this table
    pub indexes: Vec<IndexSnapshot>,

    /// Check constraints
    pub check_constraints: Vec<CheckConstraintSnapshot>,
}

/// Snapshot of a single column
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ColumnSnapshot {
    /// Column name
    pub name: String,

    /// Data type (normalized, database-agnostic representation)
    pub data_type: String,

    /// Whether column is nullable
    pub nullable: bool,

    /// Default value (if any)
    pub default_value: Option<String>,

    /// Whether column has auto-increment/sequence
    pub auto_increment: bool,
}

/// Snapshot of a primary key
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PrimaryKeySnapshot {
    /// Name of the constraint
    pub name: String,

    /// Column names that make up the primary key
    pub columns: Vec<String>,
}

/// Snapshot of a foreign key
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ForeignKeySnapshot {
    /// Name of the constraint
    pub name: String,

    /// Columns in this table
    pub columns: Vec<String>,

    /// Referenced table
    pub referenced_table: String,

    /// Referenced columns
    pub referenced_columns: Vec<String>,

    /// On delete action
    pub on_delete: Option<String>,

    /// On update action
    pub on_update: Option<String>,
}

/// Snapshot of a unique constraint
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UniqueConstraintSnapshot {
    /// Name of the constraint
    pub name: String,

    /// Column names that make up the unique constraint
    pub columns: Vec<String>,
}

/// Snapshot of an index
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IndexSnapshot {
    /// Index name
    pub name: String,

    /// Column names in this index
    pub columns: Vec<String>,

    /// Whether index is unique
    pub unique: bool,

    /// Index type (e.g., "btree", "hash", "gin")
    pub index_type: Option<String>,
}

/// Snapshot of a check constraint
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckConstraintSnapshot {
    /// Name of the constraint
    pub name: String,

    /// Check expression (normalized)
    pub expression: String,
}

/// Snapshot of a view
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ViewSnapshot {
    /// View name
    pub name: String,

    /// View definition (normalized SQL)
    pub definition: String,
}

/// Snapshot of a function/procedure
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionSnapshot {
    /// Function name
    pub name: String,

    /// Function signature (parameters)
    pub signature: String,

    /// Function body/definition
    pub body: String,

    /// Return type
    pub return_type: String,
}

/// Snapshot of a custom type (enum, composite, etc.)
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TypeSnapshot {
    /// Type name
    pub name: String,

    /// Type kind (enum, composite, domain, etc.)
    pub kind: String,

    /// Type definition (normalized)
    pub definition: String,
}

/// Trait for storing and retrieving schema snapshots
///
/// Implementations can store snapshots in:
/// - File system
/// - Database
/// - Version control
/// - In-memory (for testing)
#[async_trait]
pub trait SnapshotStore: Send + Sync {
    /// Store a snapshot for a tenant
    async fn store(&self, tenant: &crate::cli::TenantContext, snapshot: &SchemaSnapshot) -> Result<()>;

    /// Retrieve the latest snapshot for a tenant
    async fn get_latest(&self, tenant: &crate::cli::TenantContext) -> Result<Option<SchemaSnapshot>>;

    /// Retrieve a specific snapshot by version hash
    async fn get_by_hash(
        &self,
        tenant: &crate::cli::TenantContext,
        version_hash: &str,
    ) -> Result<Option<SchemaSnapshot>>;

    /// List all snapshots for a tenant (ordered by creation time, newest first)
    async fn list(&self, tenant: &crate::cli::TenantContext) -> Result<Vec<SchemaSnapshot>>;

    /// Compare two snapshots and return their version hashes
    async fn compare(
        &self,
        tenant: &crate::cli::TenantContext,
        hash_a: &str,
        hash_b: &str,
    ) -> Result<crate::diff::SchemaDiff>;
}

/// Calculate a deterministic hash for a snapshot
///
/// This function ensures that the same schema always produces the same hash,
/// regardless of the order of elements or other non-semantic differences.
pub fn calculate_version_hash(snapshot: &SchemaSnapshot) -> String {
    // In a real implementation, this would:
    // 1. Normalize the snapshot (sort maps, etc.)
    // 2. Serialize to a canonical format
    // 3. Hash using SHA-256 or similar
    // 4. Return hex-encoded hash
    
    // For now, return a placeholder
    format!("hash_{}", snapshot.tables.len())
}