bottle_orm/migration.rs
1//! # Migration Module
2//!
3//! This module provides schema migration management functionality for Bottle ORM.
4//! It handles the registration and execution of database schema changes, including
5//! table creation and foreign key constraint assignment.
6//!
7//! ## Overview
8//!
9//! The migration system follows a two-phase approach:
10//!
11//! 1. **Table Creation Phase**: Creates all registered tables with their columns,
12//! indexes, and constraints (except foreign keys)
13//! 2. **Foreign Key Phase**: Assigns foreign key constraints after all tables exist
14//!
15//! This ensures that foreign keys can reference tables that haven't been created yet.
16//!
17//! ## Features
18//!
19//! - **Automatic Ordering**: Handles dependencies between tables automatically
20//! - **Idempotent Operations**: Safe to run multiple times (uses IF NOT EXISTS)
21//! - **Type Safety**: Leverages Rust's type system for compile-time validation
22//! - **Async Execution**: Non-blocking migration execution
23//!
24//! ## Example Usage
25//!
26//! ```rust,ignore
27//! use bottle_orm::{Database, Model};
28//! use uuid::Uuid;
29//!
30//! #[derive(Model)]
31//! struct User {
32//! #[orm(primary_key)]
33//! id: Uuid,
34//! username: String,
35//! }
36//!
37//! #[derive(Model)]
38//! struct Post {
39//! #[orm(primary_key)]
40//! id: Uuid,
41//! #[orm(foreign_key = "User::id")]
42//! user_id: Uuid,
43//! title: String,
44//! }
45//!
46//! #[tokio::main]
47//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
48//! let db = Database::connect("postgres://localhost/mydb").await?;
49//!
50//! // Register and run migrations
51//! db.migrator()
52//! .register::<User>()
53//! .register::<Post>()
54//! .run()
55//! .await?;
56//!
57//! Ok(())
58//! }
59//! ```
60
61// ============================================================================
62// External Crate Imports
63// ============================================================================
64
65use futures::future::BoxFuture;
66
67// ============================================================================
68// Internal Crate Imports
69// ============================================================================
70
71use crate::{database::Database, model::Model};
72
73// ============================================================================
74// Type Aliases
75// ============================================================================
76
77/// Type alias for migration tasks (e.g., Create Table, Add Foreign Key).
78///
79/// Migration tasks are async closures that take a `Database` instance and return
80/// a boxed future that resolves to a Result. This allows for flexible, composable
81/// migration operations.
82///
83/// # Type Definition
84///
85/// ```rust,ignore
86/// type MigrationTask = Box<
87/// dyn Fn(Database) -> BoxFuture<'static, Result<(), sqlx::Error>> + Send + Sync
88/// >;
89/// ```
90///
91/// # Parameters
92///
93/// * `Database` - Cloned database instance for the migration operation
94///
95/// # Returns
96///
97/// * `BoxFuture<'static, Result<(), sqlx::Error>>` - Async result of the migration
98///
99/// # Traits
100///
101/// * `Send` - Can be safely sent between threads
102/// * `Sync` - Can be safely shared between threads
103///
104/// # Example
105///
106/// ```rust,ignore
107/// let task: MigrationTask = Box::new(|db: Database| {
108/// Box::pin(async move {
109/// db.create_table::<User>().await?;
110/// Ok(())
111/// })
112/// });
113/// ```
114pub type MigrationTask = Box<dyn Fn(Database) -> BoxFuture<'static, Result<(), sqlx::Error>> + Send + Sync>;
115
116// ============================================================================
117// Migrator Struct
118// ============================================================================
119
120/// Schema migration manager.
121///
122/// The `Migrator` is responsible for managing and executing database schema migrations.
123/// It maintains two separate task queues: one for table creation and one for foreign
124/// key assignment. This separation ensures that all tables exist before any foreign
125/// keys are created.
126///
127/// # Fields
128///
129/// * `db` - Reference to the database connection
130/// * `tasks` - Queue of table creation tasks
131/// * `fk_task` - Queue of foreign key assignment tasks
132///
133/// # Lifecycle
134///
135/// 1. Create migrator via `Database::migrator()`
136/// 2. Register models via `register::<T>()`
137/// 3. Execute all migrations via `run()`
138///
139/// # Example
140///
141/// ```rust,ignore
142/// use bottle_orm::{Database, Model};
143///
144/// #[derive(Model)]
145/// struct User {
146/// #[orm(primary_key)]
147/// id: i32,
148/// username: String,
149/// }
150///
151/// #[derive(Model)]
152/// struct Post {
153/// #[orm(primary_key)]
154/// id: i32,
155/// #[orm(foreign_key = "User::id")]
156/// user_id: i32,
157/// title: String,
158/// }
159///
160/// let db = Database::connect("sqlite::memory:").await?;
161///
162/// let result = db.migrator()
163/// .register::<User>()
164/// .register::<Post>()
165/// .run()
166/// .await?;
167/// ```
168pub struct Migrator<'a> {
169 /// Reference to the database connection.
170 ///
171 /// This is used to execute migration tasks and is cloned for each task
172 /// to allow async execution without lifetime issues.
173 pub(crate) db: &'a Database,
174
175 /// Queue of table creation tasks.
176 ///
177 /// These tasks are executed first, in the order they were registered.
178 /// Each task creates a table with its columns, indexes, and constraints
179 /// (except foreign keys).
180 pub(crate) tasks: Vec<MigrationTask>,
181
182 /// Queue of foreign key assignment tasks.
183 ///
184 /// These tasks are executed after all table creation tasks complete.
185 /// This ensures that referenced tables exist before foreign keys are created.
186 pub(crate) fk_task: Vec<MigrationTask>,
187}
188
189// ============================================================================
190// Migrator Implementation
191// ============================================================================
192
193impl<'a> Migrator<'a> {
194 // ========================================================================
195 // Constructor
196 // ========================================================================
197
198 /// Creates a new Migrator instance associated with a Database.
199 ///
200 /// This constructor initializes empty task queues for table creation
201 /// and foreign key assignment. Typically called via `Database::migrator()`
202 /// rather than directly.
203 ///
204 /// # Arguments
205 ///
206 /// * `db` - Reference to the database connection
207 ///
208 /// # Returns
209 ///
210 /// A new `Migrator` instance with empty task queues
211 ///
212 /// # Example
213 ///
214 /// ```rust,ignore
215 /// // Usually called via database method
216 /// let migrator = db.migrator();
217 ///
218 /// // Direct construction (rarely needed)
219 /// let migrator = Migrator::new(&db);
220 /// ```
221 pub fn new(db: &'a Database) -> Self {
222 Self { db, tasks: Vec::new(), fk_task: Vec::new() }
223 }
224
225 // ========================================================================
226 // Model Registration
227 // ========================================================================
228
229 /// Registers a Model for migration.
230 ///
231 /// This method queues two tasks for the specified model:
232 ///
233 /// 1. **Table Creation Task**: Creates the table with columns, indexes,
234 /// and inline constraints (PRIMARY KEY, UNIQUE, NOT NULL, etc.)
235 /// 2. **Foreign Key Task**: Assigns foreign key constraints after all
236 /// tables are created
237 ///
238 /// Multiple models can be registered by chaining calls to this method.
239 /// The tasks will be executed in the order they were registered.
240 ///
241 /// # Type Parameters
242 ///
243 /// * `T` - The Model type to register. Must implement `Model + Send + Sync + 'static`
244 ///
245 /// # Returns
246 ///
247 /// Returns `self` to enable method chaining
248 ///
249 /// # Example
250 ///
251 /// ```rust,ignore
252 /// use bottle_orm::{Database, Model};
253 /// use uuid::Uuid;
254 ///
255 /// #[derive(Model)]
256 /// struct User {
257 /// #[orm(primary_key)]
258 /// id: Uuid,
259 /// username: String,
260 /// }
261 ///
262 /// #[derive(Model)]
263 /// struct Post {
264 /// #[orm(primary_key)]
265 /// id: Uuid,
266 /// #[orm(foreign_key = "User::id")]
267 /// user_id: Uuid,
268 /// title: String,
269 /// }
270 ///
271 /// #[derive(Model)]
272 /// struct Comment {
273 /// #[orm(primary_key)]
274 /// id: Uuid,
275 /// #[orm(foreign_key = "Post::id")]
276 /// post_id: Uuid,
277 /// #[orm(foreign_key = "User::id")]
278 /// user_id: Uuid,
279 /// content: String,
280 /// }
281 ///
282 /// // Register multiple models
283 /// db.migrator()
284 /// .register::<User>() // Creates 'user' table first
285 /// .register::<Post>() // Creates 'post' table
286 /// .register::<Comment>() // Creates 'comment' table
287 /// .run() // Executes all migrations
288 /// .await?;
289 /// ```
290 ///
291 /// # Task Execution Order
292 ///
293 /// 1. User table creation
294 /// 2. Post table creation
295 /// 3. Comment table creation
296 /// 4. Post foreign keys (user_id → User.id)
297 /// 5. Comment foreign keys (post_id → Post.id, user_id → User.id)
298 ///
299 /// # See Also
300 ///
301 /// * [`run()`](#method.run) - For executing registered migrations
302 /// * [`Database::create_table()`] - For manual table creation
303 /// * [`Database::assign_foreign_keys()`] - For manual FK assignment
304 pub fn register<T>(mut self) -> Self
305 where
306 T: Model + 'static + Send + Sync,
307 {
308 // Create table creation task
309 // This task clones the database and creates the table asynchronously
310 let task = Box::new(|db: Database| -> BoxFuture<'static, Result<(), sqlx::Error>> {
311 Box::pin(async move {
312 // Create table with columns, indexes, and inline constraints
313 db.create_table::<T>().await?;
314 Ok(())
315 })
316 });
317
318 // Create foreign key assignment task
319 // This task runs after all tables are created to ensure references exist
320 let fk_task = Box::new(|db: Database| -> BoxFuture<'static, Result<(), sqlx::Error>> {
321 Box::pin(async move {
322 // Assign foreign key constraints
323 db.assign_foreign_keys::<T>().await?;
324 Ok(())
325 })
326 });
327
328 // Add tasks to their respective queues
329 self.tasks.push(task);
330 self.fk_task.push(fk_task);
331
332 // Return self for method chaining
333 self
334 }
335
336 // ========================================================================
337 // Migration Execution
338 // ========================================================================
339
340 /// Executes all registered migration tasks.
341 ///
342 /// This method runs all queued migrations in two phases:
343 ///
344 /// **Phase 1: Table Creation**
345 /// - Executes all table creation tasks in registration order
346 /// - Creates tables with columns, indexes, and inline constraints
347 /// - Uses `CREATE TABLE IF NOT EXISTS` for idempotency
348 ///
349 /// **Phase 2: Foreign Key Assignment**
350 /// - Executes all foreign key tasks in registration order
351 /// - Creates foreign key constraints between tables
352 /// - Checks for existing constraints to avoid duplicates
353 ///
354 /// If any task fails, the entire migration is aborted and an error is returned.
355 ///
356 /// # Returns
357 ///
358 /// * `Ok(Database)` - Cloned database instance on success
359 /// * `Err(sqlx::Error)` - Database error during migration
360 ///
361 /// # Error Handling
362 ///
363 /// Errors can occur for various reasons:
364 ///
365 /// - **Connection Errors**: Database connection lost during migration
366 /// - **Syntax Errors**: Invalid SQL generated (shouldn't happen with correct Model definitions)
367 /// - **Permission Errors**: Insufficient database privileges
368 /// - **Constraint Violations**: Existing data violates new constraints
369 ///
370 /// # Example
371 ///
372 /// ```rust,ignore
373 /// use bottle_orm::{Database, Model};
374 ///
375 /// #[derive(Model)]
376 /// struct User {
377 /// #[orm(primary_key)]
378 /// id: i32,
379 /// username: String,
380 /// }
381 ///
382 /// let db = Database::connect("sqlite::memory:").await?;
383 ///
384 /// // Run migrations
385 /// match db.migrator().register::<User>().run().await {
386 /// Ok(db) => println!("Migrations completed successfully"),
387 /// Err(e) => eprintln!("Migration failed: {}", e),
388 /// }
389 /// ```
390 ///
391 /// # Idempotency
392 ///
393 /// Migrations are designed to be idempotent and can be run multiple times safely:
394 ///
395 /// ```rust,ignore
396 /// // First run: creates tables
397 /// db.migrator().register::<User>().run().await?;
398 ///
399 /// // Second run: no-op (tables already exist)
400 /// db.migrator().register::<User>().run().await?;
401 /// ```
402 ///
403 /// # Performance Considerations
404 ///
405 /// - Migrations are executed sequentially, not in parallel
406 /// - Large schemas may take time to migrate
407 /// - Consider running migrations during deployment/startup
408 /// - Use database transactions where supported
409 ///
410 /// # See Also
411 ///
412 /// * [`register()`](#method.register) - For registering models
413 /// * [`Database::create_table()`] - For manual table creation
414 /// * [`Database::assign_foreign_keys()`] - For manual FK assignment
415 pub async fn run(self) -> Result<Database, sqlx::Error> {
416 // ====================================================================
417 // Phase 1: Execute Table Creation Tasks
418 // ====================================================================
419 // Create all tables in the order they were registered.
420 // This ensures that models are created before their dependents.
421 for task in self.tasks {
422 // Clone the database for the async task
423 // This is safe because Database contains a connection pool
424 (task)(self.db.clone()).await?;
425 }
426
427 // ====================================================================
428 // Phase 2: Execute Foreign Key Assignment Tasks
429 // ====================================================================
430 // Assign foreign keys after all tables exist.
431 // This prevents errors where a foreign key references a table
432 // that hasn't been created yet.
433 for task in self.fk_task {
434 // Clone the database for the async task
435 (task)(self.db.clone()).await?;
436 }
437
438 // Return cloned database instance for continued use
439 Ok(self.db.clone())
440 }
441}