libsql_orm/
migrations.rs

1//! Database migration system for libsql-orm
2//!
3//! This module provides a comprehensive migration system for managing database schema
4//! changes over time. It supports creating, executing, and tracking migrations with
5//! both manual and auto-generated approaches.
6//!
7//! # Features
8//!
9//! - **Auto-generation**: Generate migrations from model definitions
10//! - **Manual creation**: Build custom migrations with the builder pattern
11//! - **Templates**: Pre-built migration templates for common operations
12//! - **History tracking**: Track which migrations have been executed
13//! - **Rollback support**: Reverse migrations with down scripts
14//! - **Batch execution**: Run multiple migrations in sequence
15//!
16//! # Basic Usage
17//!
18//! ```rust
19//! use libsql_orm::{MigrationManager, MigrationBuilder, generate_migration};
20//!
21//! async fn run_migrations(db: Database) -> Result<(), Error> {
22//!     let manager = MigrationManager::new(db);
23//!     manager.init().await?;
24//!     
25//!     // Auto-generate from model
26//!     let migration = generate_migration!(User);
27//!     manager.execute_migration(&migration).await?;
28//!     
29//!     // Manual migration
30//!     let manual_migration = MigrationBuilder::new("add_index")
31//!         .up("CREATE INDEX idx_users_email ON users(email)")
32//!         .down("DROP INDEX idx_users_email")
33//!         .build();
34//!     
35//!     manager.execute_migration(&manual_migration).await?;
36//!     Ok(())
37//! }
38//! ```
39//!
40//! # Migration Templates
41//!
42//! ```rust
43//! use libsql_orm::templates;
44//!
45//! // Create table
46//! let create_table = templates::create_table("posts", &[
47//!     ("id", "INTEGER PRIMARY KEY AUTOINCREMENT"),
48//!     ("title", "TEXT NOT NULL"),
49//!     ("content", "TEXT"),
50//! ]);
51//!
52//! // Add column
53//! let add_column = templates::add_column("posts", "published_at", "TEXT");
54//!
55//! // Create index
56//! let create_index = templates::create_index("idx_posts_title", "posts", &["title"]);
57//! ```
58
59use crate::{database::Database, error::Error};
60use chrono::{DateTime, Utc};
61use serde::{Deserialize, Serialize};
62
63/// Represents a database migration
64///
65/// A migration contains the SQL statements needed to evolve the database schema
66/// along with metadata for tracking execution history.
67///
68/// # Examples
69///
70/// ```rust
71/// use libsql_orm::{Migration, MigrationBuilder};
72///
73/// let migration = MigrationBuilder::new("create_users_table")
74///     .up("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)")
75///     .down("DROP TABLE users")
76///     .build();
77/// ```
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct Migration {
80    pub id: String,
81    pub name: String,
82    pub sql: String,
83    pub created_at: DateTime<Utc>,
84    pub executed_at: Option<DateTime<Utc>>,
85}
86
87/// Migration manager for handling database schema changes
88///
89/// The central component for managing database migrations. Handles initialization,
90/// execution, and tracking of schema changes.
91///
92/// # Examples
93///
94/// ```rust
95/// use libsql_orm::{MigrationManager, Database};
96///
97/// async fn setup_migrations(db: Database) -> Result<(), Error> {
98///     let manager = MigrationManager::new(db);
99///     
100///     // Initialize migration tracking
101///     manager.init().await?;
102///     
103///     // Get migration status
104///     let executed = manager.get_executed_migrations().await?;
105///     let pending = manager.get_pending_migrations().await?;
106///     
107///     println!("Executed: {}, Pending: {}", executed.len(), pending.len());
108///     Ok(())
109/// }
110/// ```
111pub struct MigrationManager {
112    db: Database,
113}
114
115impl MigrationManager {
116    /// Create a new migration manager
117    pub fn new(db: Database) -> Self {
118        Self { db }
119    }
120
121    /// Initialize the migration table
122    pub async fn init(&self) -> Result<(), Error> {
123        let sql = r#"
124            CREATE TABLE IF NOT EXISTS migrations (
125                id TEXT PRIMARY KEY,
126                name TEXT NOT NULL,
127                sql TEXT NOT NULL,
128                created_at TEXT NOT NULL,
129                executed_at TEXT
130            )
131        "#;
132
133        let params = vec![libsql::Value::Null; 0];
134
135        self.db.inner.execute(sql, params).await?;
136        Ok(())
137    }
138
139    /// Create a new migration
140    pub fn create_migration(name: &str, sql: &str) -> Migration {
141        Migration {
142            id: uuid::Uuid::new_v4().to_string(),
143            name: name.to_string(),
144            sql: sql.to_string(),
145            created_at: Utc::now(),
146            executed_at: None,
147        }
148    }
149
150    /// Get all migrations from the database
151    pub async fn get_migrations(&self) -> Result<Vec<Migration>, Error> {
152        let sql =
153            "SELECT id, name, sql, created_at, executed_at FROM migrations ORDER BY created_at";
154        let mut rows = self
155            .db
156            .inner
157            .query(sql, vec![libsql::Value::Null; 0])
158            .await?;
159
160        let mut migrations = Vec::new();
161        while let Some(row) = rows.next().await? {
162            let migration = Migration {
163                id: row.get(0)?,
164                name: row.get(1)?,
165                sql: row.get(2)?,
166                created_at: DateTime::parse_from_rfc3339(&row.get::<String>(3).unwrap_or_default())
167                    .map_err(|_| Error::DatabaseError("Invalid datetime format".to_string()))?
168                    .with_timezone(&Utc),
169                executed_at: row
170                    .get::<Option<String>>(4)
171                    .unwrap_or(None)
172                    .map(|dt| {
173                        DateTime::parse_from_rfc3339(&dt)
174                            .map_err(|_| {
175                                Error::DatabaseError("Invalid datetime format".to_string())
176                            })
177                            .map(|dt| dt.with_timezone(&Utc))
178                    })
179                    .transpose()?,
180            };
181            migrations.push(migration);
182        }
183
184        Ok(migrations)
185    }
186
187    /// Execute a migration
188    pub async fn execute_migration(&self, migration: &Migration) -> Result<(), Error> {
189        // Begin transaction
190        self.db
191            .inner
192            .execute("BEGIN", vec![libsql::Value::Null; 0])
193            .await?;
194
195        // Execute the migration SQL
196        self.db
197            .inner
198            .execute(&migration.sql, vec![libsql::Value::Null; 0])
199            .await?;
200
201        // Record the migration
202        let sql = r#"
203            INSERT INTO migrations (id, name, sql, created_at, executed_at)
204            VALUES (?, ?, ?, ?, ?)
205        "#;
206
207        self.db
208            .inner
209            .execute(
210                sql,
211                vec![
212                    libsql::Value::Text(migration.id.clone()),
213                    libsql::Value::Text(migration.name.clone()),
214                    libsql::Value::Text(migration.sql.clone()),
215                    libsql::Value::Text(migration.created_at.to_rfc3339()),
216                    libsql::Value::Text(Utc::now().to_rfc3339()),
217                ],
218            )
219            .await?;
220
221        // Commit transaction
222        self.db
223            .inner
224            .execute("COMMIT", vec![libsql::Value::Null; 0])
225            .await?;
226
227        Ok(())
228    }
229
230    /// Rollback a migration
231    pub async fn rollback_migration(&self, migration_id: &str) -> Result<(), Error> {
232        let sql = "DELETE FROM migrations WHERE id = ?";
233        self.db
234            .inner
235            .execute(sql, vec![libsql::Value::Text(migration_id.to_string())])
236            .await?;
237        Ok(())
238    }
239
240    /// Get pending migrations (not yet executed)
241    pub async fn get_pending_migrations(&self) -> Result<Vec<Migration>, Error> {
242        let migrations = self.get_migrations().await?;
243        Ok(migrations
244            .into_iter()
245            .filter(|m| m.executed_at.is_none())
246            .collect())
247    }
248
249    /// Get executed migrations
250    pub async fn get_executed_migrations(&self) -> Result<Vec<Migration>, Error> {
251        let migrations = self.get_migrations().await?;
252        Ok(migrations
253            .into_iter()
254            .filter(|m| m.executed_at.is_some())
255            .collect())
256    }
257
258    /// Run all pending migrations
259    pub async fn run_migrations(&self, migrations: Vec<Migration>) -> Result<(), Error> {
260        for migration in migrations {
261            if let Some(_executed_at) = migration.executed_at {
262                continue;
263            }
264
265            self.execute_migration(&migration).await?;
266        }
267
268        Ok(())
269    }
270
271    /// Create a migration from a file
272    pub async fn create_migration_from_file(
273        name: &str,
274        file_path: &str,
275    ) -> Result<Migration, Error> {
276        let sql = std::fs::read_to_string(file_path)
277            .map_err(|e| Error::DatabaseError(format!("Failed to read migration file: {e}")))?;
278
279        Ok(Self::create_migration(name, &sql))
280    }
281
282    /// Generate a migration name from a description
283    pub fn generate_migration_name(description: &str) -> String {
284        let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
285        let sanitized_description = description
286            .to_lowercase()
287            .replace(" ", "_")
288            .replace("-", "_")
289            .chars()
290            .filter(|c| c.is_alphanumeric() || *c == '_')
291            .collect::<String>();
292
293        format!("{timestamp}_{sanitized_description}")
294    }
295
296    pub fn database(&self) -> &Database {
297        &self.db
298    }
299}
300
301/// Builder for creating migrations
302///
303/// Provides a fluent interface for constructing migrations with up and down SQL.
304///
305/// # Examples
306///
307/// ```rust
308/// use libsql_orm::MigrationBuilder;
309///
310/// let migration = MigrationBuilder::new("add_user_email_index")
311///     .up("CREATE UNIQUE INDEX idx_users_email ON users(email)")
312///     .down("DROP INDEX idx_users_email")
313///     .build();
314/// ```
315pub struct MigrationBuilder {
316    name: String,
317    up_sql: String,
318    down_sql: Option<String>,
319}
320
321impl MigrationBuilder {
322    /// Create a new migration builder
323    pub fn new(name: &str) -> Self {
324        Self {
325            name: name.to_string(),
326            up_sql: String::new(),
327            down_sql: None,
328        }
329    }
330
331    /// Add SQL for the up migration
332    pub fn up(mut self, sql: &str) -> Self {
333        self.up_sql = sql.to_string();
334        self
335    }
336
337    /// Add SQL for the down migration (rollback)
338    pub fn down(mut self, sql: &str) -> Self {
339        self.down_sql = Some(sql.to_string());
340        self
341    }
342
343    /// Build the migration
344    pub fn build(self) -> Migration {
345        Migration {
346            id: uuid::Uuid::new_v4().to_string(),
347            name: self.name,
348            sql: self.up_sql,
349            created_at: Utc::now(),
350            executed_at: None,
351        }
352    }
353}
354
355/// Common migration templates
356///
357/// Pre-built migration templates for common database operations like creating tables,
358/// adding columns, creating indexes, etc. These templates provide a quick way to
359/// generate migrations for standard operations.
360///
361/// # Examples
362///
363/// ```rust
364/// use libsql_orm::templates;
365///
366/// // Create a new table
367/// let create_table = templates::create_table("users", &[
368///     ("id", "INTEGER PRIMARY KEY AUTOINCREMENT"),
369///     ("name", "TEXT NOT NULL"),
370///     ("email", "TEXT UNIQUE NOT NULL"),
371/// ]);
372///
373/// // Add a column to existing table
374/// let add_column = templates::add_column("users", "created_at", "TEXT NOT NULL");
375///
376/// // Create an index
377/// let create_index = templates::create_index("idx_users_email", "users", &["email"]);
378/// ```
379pub mod templates {
380    use super::*;
381
382    /// Create a table migration
383    pub fn create_table(table_name: &str, columns: &[(&str, &str)]) -> Migration {
384        let column_definitions = columns
385            .iter()
386            .map(|(name, definition)| format!("{name} {definition}"))
387            .collect::<Vec<_>>()
388            .join(", ");
389
390        let sql = format!("CREATE TABLE {table_name} ({column_definitions})");
391
392        MigrationBuilder::new(&format!("create_table_{table_name}"))
393            .up(&sql)
394            .build()
395    }
396
397    /// Add column migration
398    pub fn add_column(table_name: &str, column_name: &str, definition: &str) -> Migration {
399        let sql = format!("ALTER TABLE {table_name} ADD COLUMN {column_name} {definition}");
400
401        MigrationBuilder::new(&format!("add_column_{table_name}_{column_name}"))
402            .up(&sql)
403            .build()
404    }
405
406    /// Drop column migration
407    pub fn drop_column(table_name: &str, column_name: &str) -> Migration {
408        let sql = format!("ALTER TABLE {table_name} DROP COLUMN {column_name}");
409
410        MigrationBuilder::new(&format!("drop_column_{table_name}_{column_name}"))
411            .up(&sql)
412            .build()
413    }
414
415    /// Create index migration
416    pub fn create_index(index_name: &str, table_name: &str, columns: &[&str]) -> Migration {
417        let column_list = columns.join(", ");
418        let sql = format!("CREATE INDEX {index_name} ON {table_name} ({column_list})");
419
420        MigrationBuilder::new(&format!("create_index_{index_name}"))
421            .up(&sql)
422            .build()
423    }
424
425    /// Drop index migration
426    pub fn drop_index(index_name: &str) -> Migration {
427        let sql = format!("DROP INDEX {index_name}");
428
429        MigrationBuilder::new(&format!("drop_index_{index_name}"))
430            .up(&sql)
431            .build()
432    }
433}