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(¤t, &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(¤t, &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