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}