Skip to main content

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