Skip to main content

schema_sync/
engine.rs

1//! Developer: s4gor
2//! Github: https://github.com/s4gor
3//!
4//! Main engine for schema synchronization
5//!
6//! The engine orchestrates all components to provide a high-level API
7//! for schema synchronization. It coordinates:
8//! - Schema inspection
9//! - Diff calculation
10//! - Planning
11//! - Execution
12//! - Snapshot management
13//!
14//! ## Design Rationale
15//!
16//! The engine provides a unified interface that hides the complexity
17//! of coordinating multiple components. It's mode-agnostic: the same
18//! engine can be used for sync, dry-run, validation, and audit modes.
19//! Mode-specific behavior is handled at the CLI layer.
20
21use crate::adapters::{DatabaseAdapter, MigrationRunner, SchemaInspector};
22use crate::diff::{DiffCalculator, SchemaDiff};
23use crate::errors::Result;
24use crate::executor::Executor;
25use crate::planner::Planner;
26use crate::snapshot::{SchemaSnapshot, SnapshotStore};
27
28/// Result of a sync operation
29#[derive(Debug, Clone)]
30pub struct SyncResult {
31    /// Whether schemas were already in sync
32    pub already_in_sync: bool,
33
34    /// Diff that was calculated
35    pub diff: SchemaDiff,
36
37    /// Number of changes applied (0 in dry-run mode)
38    pub changes_applied: usize,
39
40    /// Execution result (if execution was attempted)
41    pub execution_result: Option<crate::executor::ExecutionResult>,
42}
43
44/// Main engine for schema synchronization
45///
46/// The engine coordinates all components to provide schema sync functionality.
47/// It's designed to be used by the CLI layer, which handles mode-specific
48/// behavior (dry-run, validation, etc.).
49pub struct Engine {
50    inspector: Box<dyn SchemaInspector>,
51    runner: Box<dyn MigrationRunner>,
52    planner: Box<dyn Planner>,
53    executor: Box<dyn Executor>,
54    diff_calculator: Box<dyn DiffCalculator>,
55    snapshot_store: Option<Box<dyn SnapshotStore>>,
56}
57
58impl Engine {
59    /// Create a new engine
60    ///
61    /// # Arguments
62    ///
63    /// * `inspector` - Schema inspector for reading current schema
64    /// * `runner` - Migration runner for executing changes
65    /// * `planner` - Planner for creating migration plans
66    /// * `executor` - Executor for orchestrating execution
67    /// * `diff_calculator` - Calculator for computing diffs
68    /// * `snapshot_store` - Optional snapshot store for versioning
69    pub fn new(
70        inspector: Box<dyn SchemaInspector>,
71        runner: Box<dyn MigrationRunner>,
72        planner: Box<dyn Planner>,
73        executor: Box<dyn Executor>,
74        diff_calculator: Box<dyn DiffCalculator>,
75        snapshot_store: Option<Box<dyn SnapshotStore>>,
76    ) -> Self {
77        Self {
78            inspector,
79            runner,
80            planner,
81            executor,
82            diff_calculator,
83            snapshot_store,
84        }
85    }
86
87    /// Create an engine from a database adapter
88    ///
89    /// This is a convenience method that creates an engine with
90    /// default implementations of planner, executor, and diff calculator.
91    pub fn from_adapter(
92        adapter: Box<dyn DatabaseAdapter>,
93        planner: Box<dyn Planner>,
94        executor: Box<dyn Executor>,
95        diff_calculator: Box<dyn DiffCalculator>,
96        snapshot_store: Option<Box<dyn SnapshotStore>>,
97    ) -> Self {
98        Self::new(
99            adapter.inspector(),
100            adapter.migration_runner(),
101            planner,
102            executor,
103            diff_calculator,
104            snapshot_store,
105        )
106    }
107
108    /// Sync a tenant's schema to match a target snapshot
109    ///
110    /// This is the main operation. It:
111    /// 1. Inspects the current schema
112    /// 2. Calculates the diff to the target
113    /// 3. Creates a migration plan
114    /// 4. Optionally executes the plan
115    ///
116    /// # Arguments
117    ///
118    /// * `tenant` - The tenant context
119    /// * `target` - Target schema snapshot (or None to use stored snapshot)
120    /// * `execute` - Whether to actually execute the migration (false for dry-run)
121    ///
122    /// # Returns
123    ///
124    /// Sync result with diff and execution details.
125    pub async fn sync_tenant(
126        &self,
127        tenant: &crate::cli::TenantContext,
128        target: Option<&SchemaSnapshot>,
129        execute: bool,
130    ) -> Result<SyncResult> {
131        // Inspect current schema
132        let current = self.inspector.inspect_schema(tenant).await?;
133
134        // Get target schema
135        let target = match target {
136            Some(t) => t.clone(),
137            None => {
138                // Try to get from snapshot store
139                match &self.snapshot_store {
140                    Some(store) => {
141                        store.get_latest(tenant).await?
142                            .ok_or_else(|| crate::errors::Error::Snapshot(
143                                format!("No target snapshot found for tenant {}", tenant.id())
144                            ))?
145                    }
146                    None => {
147                        return Err(crate::errors::Error::Snapshot(
148                            "No target snapshot provided and no snapshot store configured".to_string()
149                        ));
150                    }
151                }
152            }
153        };
154
155        // Calculate diff
156        let diff = self.diff_calculator.calculate_diff(&current, &target);
157
158        // Check if already in sync
159        if diff.is_empty() {
160            return Ok(SyncResult {
161                already_in_sync: true,
162                diff,
163                changes_applied: 0,
164                execution_result: None,
165            });
166        }
167
168        // Create migration plan
169        let plan = self.planner.create_plan(&current, &target, &diff).await?;
170
171        // Validate plan
172        self.planner.validate_plan(&plan).await?;
173
174        // Execute if requested
175        let execution_result = if execute {
176            Some(self.executor.execute(tenant, &plan, self.runner.as_ref()).await?)
177        } else {
178            Some(self.executor.dry_run(tenant, &plan, self.runner.as_ref()).await?)
179        };
180
181        Ok(SyncResult {
182            already_in_sync: false,
183            diff,
184            changes_applied: execution_result.as_ref()
185                .map(|r| r.steps_executed)
186                .unwrap_or(0),
187            execution_result,
188        })
189    }
190
191    /// Get the current schema snapshot for a tenant
192    pub async fn inspect_tenant(&self, tenant: &crate::cli::TenantContext) -> Result<SchemaSnapshot> {
193        self.inspector.inspect_schema(tenant).await
194    }
195
196    /// Calculate diff between two snapshots
197    pub fn calculate_diff(&self, from: &SchemaSnapshot, to: &SchemaSnapshot) -> SchemaDiff {
198        self.diff_calculator.calculate_diff(from, to)
199    }
200
201    /// List all tenants
202    pub async fn list_tenants(&self) -> Result<Vec<crate::cli::TenantContext>> {
203        self.inspector.list_tenants().await
204    }
205}
206