Skip to main content

cargo_hammerwork/utils/
database.rs

1//! Database connection and migration utilities.
2//!
3//! This module provides abstractions for working with both PostgreSQL and MySQL
4//! databases in the Hammerwork CLI. It handles connection pooling, automatic
5//! database type detection, and migration management.
6//!
7//! # Examples
8//!
9//! ## Connecting to a Database
10//!
11//! ```rust,no_run
12//! use cargo_hammerwork::utils::database::DatabasePool;
13//!
14//! # #[tokio::main]
15//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
16//! // Connect to PostgreSQL
17//! let pg_pool = DatabasePool::connect(
18//!     "postgresql://localhost/hammerwork",
19//!     5  // connection pool size
20//! ).await?;
21//!
22//! // Connect to MySQL
23//! let mysql_pool = DatabasePool::connect(
24//!     "mysql://localhost/hammerwork",
25//!     5
26//! ).await?;
27//! # Ok(())
28//! # }
29//! ```
30//!
31//! ## Running Migrations
32//!
33//! ```rust,no_run
34//! use cargo_hammerwork::utils::database::DatabasePool;
35//!
36//! # #[tokio::main]
37//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
38//! let pool = DatabasePool::connect("postgresql://localhost/hammerwork", 5).await?;
39//!
40//! // Run all pending migrations
41//! pool.migrate(false).await?;
42//!
43//! // Drop existing tables and run migrations from scratch
44//! pool.migrate(true).await?;
45//! # Ok(())
46//! # }
47//! ```
48//!
49//! ## Creating a Job Queue
50//!
51//! ```rust,no_run
52//! use cargo_hammerwork::utils::database::DatabasePool;
53//!
54//! # #[tokio::main]
55//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
56//! let pool = DatabasePool::connect("postgresql://localhost/hammerwork", 5).await?;
57//! let job_queue = pool.create_job_queue();
58//!
59//! // Now you can use the job queue for operations
60//! // The wrapper automatically handles PostgreSQL vs MySQL differences
61//! # Ok(())
62//! # }
63//! ```
64
65use anyhow::Result;
66use hammerwork::{
67    JobQueue,
68    migrations::{
69        MigrationManager, mysql::MySqlMigrationRunner, postgres::PostgresMigrationRunner,
70    },
71};
72use sqlx::{MySqlPool, PgPool};
73use tracing::info;
74
75/// Database connection pool abstraction.
76///
77/// This enum wraps either a PostgreSQL or MySQL connection pool,
78/// providing a unified interface for database operations.
79///
80/// # Database URL Format
81///
82/// - PostgreSQL: `postgres://` or `postgresql://`
83/// - MySQL: `mysql://`
84///
85/// # Examples
86///
87/// ```rust,no_run
88/// use cargo_hammerwork::utils::database::DatabasePool;
89///
90/// # #[tokio::main]
91/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
92/// // The database type is automatically detected from the URL
93/// let pool = DatabasePool::connect(
94///     "postgresql://user:pass@localhost:5432/hammerwork",
95///     10  // max connections
96/// ).await?;
97/// # Ok(())
98/// # }
99/// ```
100pub enum DatabasePool {
101    Postgres(PgPool),
102    MySQL(MySqlPool),
103}
104
105impl DatabasePool {
106    /// Connect to a database with the specified connection pool size.
107    ///
108    /// The database type is automatically detected from the URL scheme:
109    /// - `postgres://` or `postgresql://` → PostgreSQL
110    /// - `mysql://` → MySQL
111    ///
112    /// # Arguments
113    ///
114    /// * `database_url` - Database connection URL
115    /// * `pool_size` - Maximum number of connections in the pool
116    ///
117    /// # Examples
118    ///
119    /// ```rust,no_run
120    /// use cargo_hammerwork::utils::database::DatabasePool;
121    ///
122    /// # #[tokio::main]
123    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
124    /// // PostgreSQL with 10 connections
125    /// let pg_pool = DatabasePool::connect(
126    ///     "postgresql://user:pass@localhost/mydb",
127    ///     10
128    /// ).await?;
129    ///
130    /// // MySQL with 5 connections
131    /// let mysql_pool = DatabasePool::connect(
132    ///     "mysql://root:pass@localhost/mydb",
133    ///     5
134    /// ).await?;
135    /// # Ok(())
136    /// # }
137    /// ```
138    pub async fn connect(database_url: &str, pool_size: u32) -> Result<Self> {
139        if database_url.starts_with("postgres://") || database_url.starts_with("postgresql://") {
140            let pool = sqlx::postgres::PgPoolOptions::new()
141                .max_connections(pool_size)
142                .connect(database_url)
143                .await?;
144            Ok(DatabasePool::Postgres(pool))
145        } else if database_url.starts_with("mysql://") {
146            let pool = sqlx::mysql::MySqlPoolOptions::new()
147                .max_connections(pool_size)
148                .connect(database_url)
149                .await?;
150            Ok(DatabasePool::MySQL(pool))
151        } else {
152            Err(anyhow::anyhow!(
153                "Unsupported database URL format. Use postgres:// or mysql://"
154            ))
155        }
156    }
157
158    /// Create a job queue from this database pool.
159    ///
160    /// This consumes the pool and returns a wrapped JobQueue that
161    /// automatically handles database-specific implementations.
162    ///
163    /// # Examples
164    ///
165    /// ```rust,no_run
166    /// use cargo_hammerwork::utils::database::DatabasePool;
167    ///
168    /// # #[tokio::main]
169    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
170    /// let pool = DatabasePool::connect("postgresql://localhost/hammerwork", 5).await?;
171    /// let job_queue = pool.create_job_queue();
172    /// // Use job_queue for enqueuing, processing, etc.
173    /// # Ok(())
174    /// # }
175    /// ```
176    pub fn create_job_queue(self) -> JobQueueWrapper {
177        match self {
178            DatabasePool::Postgres(pool) => JobQueueWrapper::Postgres(JobQueue::new(pool)),
179            DatabasePool::MySQL(pool) => JobQueueWrapper::MySQL(JobQueue::new(pool)),
180        }
181    }
182
183    /// Run database migrations.
184    ///
185    /// This method runs all pending migrations on the connected database.
186    /// Optionally drops existing tables before running migrations.
187    ///
188    /// # Arguments
189    ///
190    /// * `drop_tables` - If true, drops all Hammerwork tables before running migrations
191    ///
192    /// # Examples
193    ///
194    /// ```rust,no_run
195    /// use cargo_hammerwork::utils::database::DatabasePool;
196    ///
197    /// # #[tokio::main]
198    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
199    /// let pool = DatabasePool::connect("postgresql://localhost/hammerwork", 5).await?;
200    ///
201    /// // Run migrations on existing database
202    /// pool.migrate(false).await?;
203    ///
204    /// // Drop tables and run fresh migrations
205    /// pool.migrate(true).await?;
206    /// # Ok(())
207    /// # }
208    /// ```
209    pub async fn migrate(&self, drop_tables: bool) -> Result<()> {
210        match self {
211            DatabasePool::Postgres(pool) => {
212                if drop_tables {
213                    info!("Dropping existing PostgreSQL tables...");
214                    sqlx::query("DROP TABLE IF EXISTS hammerwork_jobs CASCADE")
215                        .execute(pool)
216                        .await?;
217                    sqlx::query("DROP TABLE IF EXISTS hammerwork_migrations CASCADE")
218                        .execute(pool)
219                        .await?;
220                    sqlx::query("DROP TABLE IF EXISTS hammerwork_workflows CASCADE")
221                        .execute(pool)
222                        .await?;
223                }
224
225                info!("Running PostgreSQL migrations...");
226                let runner = Box::new(PostgresMigrationRunner::new(pool.clone()));
227                let manager = MigrationManager::new(runner);
228                manager.run_migrations().await?;
229                info!("PostgreSQL migrations completed successfully");
230            }
231            DatabasePool::MySQL(pool) => {
232                if drop_tables {
233                    info!("Dropping existing MySQL tables...");
234                    sqlx::query("DROP TABLE IF EXISTS hammerwork_jobs")
235                        .execute(pool)
236                        .await?;
237                    sqlx::query("DROP TABLE IF EXISTS hammerwork_migrations")
238                        .execute(pool)
239                        .await?;
240                    sqlx::query("DROP TABLE IF EXISTS hammerwork_workflows")
241                        .execute(pool)
242                        .await?;
243                }
244
245                info!("Running MySQL migrations...");
246                let runner = Box::new(MySqlMigrationRunner::new(pool.clone()));
247                let manager = MigrationManager::new(runner);
248                manager.run_migrations().await?;
249                info!("MySQL migrations completed successfully");
250            }
251        }
252        Ok(())
253    }
254}
255
256/// Wrapper for database-specific JobQueue implementations.
257///
258/// This enum provides a unified interface for working with job queues
259/// regardless of the underlying database type.
260///
261/// # Examples
262///
263/// ```rust,no_run
264/// use cargo_hammerwork::utils::database::{DatabasePool, JobQueueWrapper};
265///
266/// # #[tokio::main]
267/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
268/// let pool = DatabasePool::connect("postgresql://localhost/hammerwork", 5).await?;
269/// let job_queue = pool.create_job_queue();
270///
271/// // The wrapper handles database-specific operations internally
272/// match job_queue {
273///     JobQueueWrapper::Postgres(queue) => {
274///         // PostgreSQL-specific operations
275///     }
276///     JobQueueWrapper::MySQL(queue) => {
277///         // MySQL-specific operations
278///     }
279/// }
280/// # Ok(())
281/// # }
282/// ```
283pub enum JobQueueWrapper {
284    Postgres(JobQueue<sqlx::Postgres>),
285    MySQL(JobQueue<sqlx::MySql>),
286}