schema-sync 1.0.0

Production-grade schema synchronization for multi-tenant databases
Documentation
//! Developer: s4gor
//! Github: https://github.com/s4gor
//!
//! Database adapter traits
//!
//! This module defines the core traits that all database implementations
//! must provide. These traits enable:
//! - Multi-database support (PostgreSQL, MySQL, SQLite, etc.)
//! - Pluggable migration runners
//! - Database-agnostic schema inspection
//!
//! ## Design Rationale
//!
//! We separate concerns into three main traits:
//!
//! 1. **DatabaseAdapter**: Connection management and factory for other adapters
//! 2. **SchemaInspector**: Read-only schema introspection
//! 3. **MigrationRunner**: Write operations for applying migrations
//!
//! This separation allows:
//! - Read-only operations (inspection, audit) without write capabilities
//! - Different migration strategies (SQL files, Rust code, external tools)
//! - Testing with mock implementations
//! - Composition of different adapters

use async_trait::async_trait;

use crate::errors::Result;
use crate::snapshot::SchemaSnapshot;

/// Main database adapter trait
///
/// This is the entry point for database operations. It provides:
/// - Connection management
/// - Factory methods for inspectors and runners
/// - Database-specific configuration
///
/// Each database type (PostgreSQL, MySQL, etc.) implements this trait.
#[async_trait]
pub trait DatabaseAdapter: Send + Sync {
    /// Get a schema inspector for this database
    ///
    /// The inspector is used for read-only schema introspection.
    fn inspector(&self) -> Box<dyn SchemaInspector>;

    /// Get a migration runner for this database
    ///
    /// The runner is used for executing schema changes.
    fn migration_runner(&self) -> Box<dyn MigrationRunner>;

    /// Get the database type identifier
    ///
    /// Returns a string like "postgresql", "mysql", "sqlite", etc.
    fn database_type(&self) -> &str;

    /// Test the database connection
    async fn test_connection(&self) -> Result<()>;
}

/// Schema inspector trait for read-only schema introspection
///
/// This trait abstracts the process of reading schema information
/// from a database. Implementations are database-specific but produce
/// database-agnostic `SchemaSnapshot` objects.
///
/// ## Design Rationale
///
/// Separating inspection from execution allows:
/// - Audit mode to work without write permissions
/// - Dry-run mode to calculate diffs without locks
/// - Testing with mock inspectors
/// - Different inspection strategies (cached, streaming, etc.)
#[async_trait]
pub trait SchemaInspector: Send + Sync {
    /// Inspect the current schema for a tenant
    ///
    /// Returns a normalized snapshot of the schema as it currently exists
    /// in the database.
    ///
    /// # Arguments
    ///
    /// * `tenant` - The tenant context to inspect
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - The tenant schema doesn't exist
    /// - Database connection fails
    /// - Schema introspection fails
    async fn inspect_schema(&self, tenant: &crate::cli::TenantContext) -> Result<SchemaSnapshot>;

    /// Check if a tenant schema exists
    async fn schema_exists(&self, tenant: &crate::cli::TenantContext) -> Result<bool>;

    /// List all tenant schemas in the database
    ///
    /// This is useful for batch operations across all tenants.
    async fn list_tenants(&self) -> Result<Vec<crate::cli::TenantContext>>;
}

/// Migration runner trait for executing schema changes
///
/// This trait abstracts the execution of schema migrations. Different
/// implementations can support:
/// - SQL file-based migrations
/// - Rust code-based migrations
/// - External tool integration (diesel, sqlx migrations)
/// - Custom migration strategies
///
/// ## Design Rationale
///
/// Separating migration execution from inspection allows:
/// - Pluggable migration engines
/// - Different migration strategies per database
/// - Testing with mock runners
/// - Support for external migration tools
#[async_trait]
pub trait MigrationRunner: Send + Sync {
    /// Execute a migration plan for a tenant
    ///
    /// # Arguments
    ///
    /// * `tenant` - The tenant context
    /// * `plan` - The migration plan to execute
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Migration execution fails
    /// - Transaction rollback is needed and fails
    /// - Lock acquisition fails
    async fn execute_migration(
        &self,
        tenant: &crate::cli::TenantContext,
        plan: &crate::planner::MigrationPlan,
    ) -> Result<MigrationResult>;

    /// Acquire a lock for a tenant schema
    ///
    /// This prevents concurrent migrations on the same tenant.
    /// The lock should be released when the migration completes or fails.
    ///
    /// # Arguments
    ///
    /// * `tenant` - The tenant context
    /// * `timeout` - Maximum time to wait for lock (in seconds)
    ///
    /// # Returns
    ///
    /// Returns a lock guard that will release the lock when dropped.
    /// The lock guard must be explicitly released by calling `release()`.
    async fn acquire_lock(
        &self,
        tenant: &crate::cli::TenantContext,
        timeout_secs: u64,
    ) -> Result<Box<dyn LockGuard + Send + Sync>>;

    /// Check if a migration can be executed safely
    ///
    /// This performs pre-flight checks like:
    /// - Verifying the tenant schema exists
    /// - Checking for blocking operations
    /// - Validating migration plan compatibility
    async fn validate_migration(
        &self,
        tenant: &crate::cli::TenantContext,
        plan: &crate::planner::MigrationPlan,
    ) -> Result<()>;
}

/// Result of a migration execution
#[derive(Debug, Clone)]
pub struct MigrationResult {
    /// Number of changes applied
    pub changes_applied: usize,

    /// Whether a transaction was used
    pub used_transaction: bool,

    /// Duration of the migration
    pub duration_secs: f64,
}

/// Lock guard for tenant schema locks
///
/// When dropped, the lock should be automatically released.
/// Note: This trait uses async methods, so it cannot be used as a trait object
/// directly. Implementations should provide a concrete type that can be
/// boxed and used with `acquire_lock`.
pub trait LockGuard: Send + Sync {
    /// Explicitly release the lock
    ///
    /// This should be called explicitly to release the lock.
    /// Some implementations may also release on drop, but it's not guaranteed.
    fn release_sync(&mut self) -> Result<()>;
}