schema_sync/adapters.rs
1//! Developer: s4gor
2//! Github: https://github.com/s4gor
3//!
4//! Database adapter traits
5//!
6//! This module defines the core traits that all database implementations
7//! must provide. These traits enable:
8//! - Multi-database support (PostgreSQL, MySQL, SQLite, etc.)
9//! - Pluggable migration runners
10//! - Database-agnostic schema inspection
11//!
12//! ## Design Rationale
13//!
14//! We separate concerns into three main traits:
15//!
16//! 1. **DatabaseAdapter**: Connection management and factory for other adapters
17//! 2. **SchemaInspector**: Read-only schema introspection
18//! 3. **MigrationRunner**: Write operations for applying migrations
19//!
20//! This separation allows:
21//! - Read-only operations (inspection, audit) without write capabilities
22//! - Different migration strategies (SQL files, Rust code, external tools)
23//! - Testing with mock implementations
24//! - Composition of different adapters
25
26use async_trait::async_trait;
27
28use crate::errors::Result;
29use crate::snapshot::SchemaSnapshot;
30
31/// Main database adapter trait
32///
33/// This is the entry point for database operations. It provides:
34/// - Connection management
35/// - Factory methods for inspectors and runners
36/// - Database-specific configuration
37///
38/// Each database type (PostgreSQL, MySQL, etc.) implements this trait.
39#[async_trait]
40pub trait DatabaseAdapter: Send + Sync {
41 /// Get a schema inspector for this database
42 ///
43 /// The inspector is used for read-only schema introspection.
44 fn inspector(&self) -> Box<dyn SchemaInspector>;
45
46 /// Get a migration runner for this database
47 ///
48 /// The runner is used for executing schema changes.
49 fn migration_runner(&self) -> Box<dyn MigrationRunner>;
50
51 /// Get the database type identifier
52 ///
53 /// Returns a string like "postgresql", "mysql", "sqlite", etc.
54 fn database_type(&self) -> &str;
55
56 /// Test the database connection
57 async fn test_connection(&self) -> Result<()>;
58}
59
60/// Schema inspector trait for read-only schema introspection
61///
62/// This trait abstracts the process of reading schema information
63/// from a database. Implementations are database-specific but produce
64/// database-agnostic `SchemaSnapshot` objects.
65///
66/// ## Design Rationale
67///
68/// Separating inspection from execution allows:
69/// - Audit mode to work without write permissions
70/// - Dry-run mode to calculate diffs without locks
71/// - Testing with mock inspectors
72/// - Different inspection strategies (cached, streaming, etc.)
73#[async_trait]
74pub trait SchemaInspector: Send + Sync {
75 /// Inspect the current schema for a tenant
76 ///
77 /// Returns a normalized snapshot of the schema as it currently exists
78 /// in the database.
79 ///
80 /// # Arguments
81 ///
82 /// * `tenant` - The tenant context to inspect
83 ///
84 /// # Errors
85 ///
86 /// Returns an error if:
87 /// - The tenant schema doesn't exist
88 /// - Database connection fails
89 /// - Schema introspection fails
90 async fn inspect_schema(&self, tenant: &crate::cli::TenantContext) -> Result<SchemaSnapshot>;
91
92 /// Check if a tenant schema exists
93 async fn schema_exists(&self, tenant: &crate::cli::TenantContext) -> Result<bool>;
94
95 /// List all tenant schemas in the database
96 ///
97 /// This is useful for batch operations across all tenants.
98 async fn list_tenants(&self) -> Result<Vec<crate::cli::TenantContext>>;
99}
100
101/// Migration runner trait for executing schema changes
102///
103/// This trait abstracts the execution of schema migrations. Different
104/// implementations can support:
105/// - SQL file-based migrations
106/// - Rust code-based migrations
107/// - External tool integration (diesel, sqlx migrations)
108/// - Custom migration strategies
109///
110/// ## Design Rationale
111///
112/// Separating migration execution from inspection allows:
113/// - Pluggable migration engines
114/// - Different migration strategies per database
115/// - Testing with mock runners
116/// - Support for external migration tools
117#[async_trait]
118pub trait MigrationRunner: Send + Sync {
119 /// Execute a migration plan for a tenant
120 ///
121 /// # Arguments
122 ///
123 /// * `tenant` - The tenant context
124 /// * `plan` - The migration plan to execute
125 ///
126 /// # Errors
127 ///
128 /// Returns an error if:
129 /// - Migration execution fails
130 /// - Transaction rollback is needed and fails
131 /// - Lock acquisition fails
132 async fn execute_migration(
133 &self,
134 tenant: &crate::cli::TenantContext,
135 plan: &crate::planner::MigrationPlan,
136 ) -> Result<MigrationResult>;
137
138 /// Acquire a lock for a tenant schema
139 ///
140 /// This prevents concurrent migrations on the same tenant.
141 /// The lock should be released when the migration completes or fails.
142 ///
143 /// # Arguments
144 ///
145 /// * `tenant` - The tenant context
146 /// * `timeout` - Maximum time to wait for lock (in seconds)
147 ///
148 /// # Returns
149 ///
150 /// Returns a lock guard that will release the lock when dropped.
151 /// The lock guard must be explicitly released by calling `release()`.
152 async fn acquire_lock(
153 &self,
154 tenant: &crate::cli::TenantContext,
155 timeout_secs: u64,
156 ) -> Result<Box<dyn LockGuard + Send + Sync>>;
157
158 /// Check if a migration can be executed safely
159 ///
160 /// This performs pre-flight checks like:
161 /// - Verifying the tenant schema exists
162 /// - Checking for blocking operations
163 /// - Validating migration plan compatibility
164 async fn validate_migration(
165 &self,
166 tenant: &crate::cli::TenantContext,
167 plan: &crate::planner::MigrationPlan,
168 ) -> Result<()>;
169}
170
171/// Result of a migration execution
172#[derive(Debug, Clone)]
173pub struct MigrationResult {
174 /// Number of changes applied
175 pub changes_applied: usize,
176
177 /// Whether a transaction was used
178 pub used_transaction: bool,
179
180 /// Duration of the migration
181 pub duration_secs: f64,
182}
183
184/// Lock guard for tenant schema locks
185///
186/// When dropped, the lock should be automatically released.
187/// Note: This trait uses async methods, so it cannot be used as a trait object
188/// directly. Implementations should provide a concrete type that can be
189/// boxed and used with `acquire_lock`.
190pub trait LockGuard: Send + Sync {
191 /// Explicitly release the lock
192 ///
193 /// This should be called explicitly to release the lock.
194 /// Some implementations may also release on drop, but it's not guaranteed.
195 fn release_sync(&mut self) -> Result<()>;
196}
197