schema-sync 1.0.0

Production-grade schema synchronization for multi-tenant databases
Documentation
//! Developer: s4gor
//! Github: https://github.com/s4gor
//!
//! Main engine for schema synchronization
//!
//! The engine orchestrates all components to provide a high-level API
//! for schema synchronization. It coordinates:
//! - Schema inspection
//! - Diff calculation
//! - Planning
//! - Execution
//! - Snapshot management
//!
//! ## Design Rationale
//!
//! The engine provides a unified interface that hides the complexity
//! of coordinating multiple components. It's mode-agnostic: the same
//! engine can be used for sync, dry-run, validation, and audit modes.
//! Mode-specific behavior is handled at the CLI layer.

use crate::adapters::{DatabaseAdapter, MigrationRunner, SchemaInspector};
use crate::diff::{DiffCalculator, SchemaDiff};
use crate::errors::Result;
use crate::executor::Executor;
use crate::planner::Planner;
use crate::snapshot::{SchemaSnapshot, SnapshotStore};

/// Result of a sync operation
#[derive(Debug, Clone)]
pub struct SyncResult {
    /// Whether schemas were already in sync
    pub already_in_sync: bool,

    /// Diff that was calculated
    pub diff: SchemaDiff,

    /// Number of changes applied (0 in dry-run mode)
    pub changes_applied: usize,

    /// Execution result (if execution was attempted)
    pub execution_result: Option<crate::executor::ExecutionResult>,
}

/// Main engine for schema synchronization
///
/// The engine coordinates all components to provide schema sync functionality.
/// It's designed to be used by the CLI layer, which handles mode-specific
/// behavior (dry-run, validation, etc.).
pub struct Engine {
    inspector: Box<dyn SchemaInspector>,
    runner: Box<dyn MigrationRunner>,
    planner: Box<dyn Planner>,
    executor: Box<dyn Executor>,
    diff_calculator: Box<dyn DiffCalculator>,
    snapshot_store: Option<Box<dyn SnapshotStore>>,
}

impl Engine {
    /// Create a new engine
    ///
    /// # Arguments
    ///
    /// * `inspector` - Schema inspector for reading current schema
    /// * `runner` - Migration runner for executing changes
    /// * `planner` - Planner for creating migration plans
    /// * `executor` - Executor for orchestrating execution
    /// * `diff_calculator` - Calculator for computing diffs
    /// * `snapshot_store` - Optional snapshot store for versioning
    pub fn new(
        inspector: Box<dyn SchemaInspector>,
        runner: Box<dyn MigrationRunner>,
        planner: Box<dyn Planner>,
        executor: Box<dyn Executor>,
        diff_calculator: Box<dyn DiffCalculator>,
        snapshot_store: Option<Box<dyn SnapshotStore>>,
    ) -> Self {
        Self {
            inspector,
            runner,
            planner,
            executor,
            diff_calculator,
            snapshot_store,
        }
    }

    /// Create an engine from a database adapter
    ///
    /// This is a convenience method that creates an engine with
    /// default implementations of planner, executor, and diff calculator.
    pub fn from_adapter(
        adapter: Box<dyn DatabaseAdapter>,
        planner: Box<dyn Planner>,
        executor: Box<dyn Executor>,
        diff_calculator: Box<dyn DiffCalculator>,
        snapshot_store: Option<Box<dyn SnapshotStore>>,
    ) -> Self {
        Self::new(
            adapter.inspector(),
            adapter.migration_runner(),
            planner,
            executor,
            diff_calculator,
            snapshot_store,
        )
    }

    /// Sync a tenant's schema to match a target snapshot
    ///
    /// This is the main operation. It:
    /// 1. Inspects the current schema
    /// 2. Calculates the diff to the target
    /// 3. Creates a migration plan
    /// 4. Optionally executes the plan
    ///
    /// # Arguments
    ///
    /// * `tenant` - The tenant context
    /// * `target` - Target schema snapshot (or None to use stored snapshot)
    /// * `execute` - Whether to actually execute the migration (false for dry-run)
    ///
    /// # Returns
    ///
    /// Sync result with diff and execution details.
    pub async fn sync_tenant(
        &self,
        tenant: &crate::cli::TenantContext,
        target: Option<&SchemaSnapshot>,
        execute: bool,
    ) -> Result<SyncResult> {
        // Inspect current schema
        let current = self.inspector.inspect_schema(tenant).await?;

        // Get target schema
        let target = match target {
            Some(t) => t.clone(),
            None => {
                // Try to get from snapshot store
                match &self.snapshot_store {
                    Some(store) => {
                        store.get_latest(tenant).await?
                            .ok_or_else(|| crate::errors::Error::Snapshot(
                                format!("No target snapshot found for tenant {}", tenant.id())
                            ))?
                    }
                    None => {
                        return Err(crate::errors::Error::Snapshot(
                            "No target snapshot provided and no snapshot store configured".to_string()
                        ));
                    }
                }
            }
        };

        // Calculate diff
        let diff = self.diff_calculator.calculate_diff(&current, &target);

        // Check if already in sync
        if diff.is_empty() {
            return Ok(SyncResult {
                already_in_sync: true,
                diff,
                changes_applied: 0,
                execution_result: None,
            });
        }

        // Create migration plan
        let plan = self.planner.create_plan(&current, &target, &diff).await?;

        // Validate plan
        self.planner.validate_plan(&plan).await?;

        // Execute if requested
        let execution_result = if execute {
            Some(self.executor.execute(tenant, &plan, self.runner.as_ref()).await?)
        } else {
            Some(self.executor.dry_run(tenant, &plan, self.runner.as_ref()).await?)
        };

        Ok(SyncResult {
            already_in_sync: false,
            diff,
            changes_applied: execution_result.as_ref()
                .map(|r| r.steps_executed)
                .unwrap_or(0),
            execution_result,
        })
    }

    /// Get the current schema snapshot for a tenant
    pub async fn inspect_tenant(&self, tenant: &crate::cli::TenantContext) -> Result<SchemaSnapshot> {
        self.inspector.inspect_schema(tenant).await
    }

    /// Calculate diff between two snapshots
    pub fn calculate_diff(&self, from: &SchemaSnapshot, to: &SchemaSnapshot) -> SchemaDiff {
        self.diff_calculator.calculate_diff(from, to)
    }

    /// List all tenants
    pub async fn list_tenants(&self) -> Result<Vec<crate::cli::TenantContext>> {
        self.inspector.list_tenants().await
    }
}