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, compat::{null_value, text_value}};
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![null_value(); 0];
134
135        self.db.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        #[cfg(not(feature = "libsql"))]
153        {
154            // Return empty migrations for WASM-only builds
155            return Ok(vec![]);
156        }
157
158        #[cfg(feature = "libsql")]
159        {
160            let sql =
161                "SELECT id, name, sql, created_at, executed_at FROM migrations ORDER BY created_at";
162            let mut rows = self
163                .db
164                .query(sql, vec![null_value(); 0])
165                .await?;
166
167        let mut migrations = Vec::new();
168        while let Some(row) = rows.next().await? {
169            let migration = Migration {
170                id: row.get(0)?,
171                name: row.get(1)?,
172                sql: row.get(2)?,
173                created_at: DateTime::parse_from_rfc3339(&row.get::<String>(3).unwrap_or_default())
174                    .map_err(|_| Error::DatabaseError("Invalid datetime format".to_string()))?
175                    .with_timezone(&Utc),
176                executed_at: row
177                    .get::<Option<String>>(4)
178                    .unwrap_or(None)
179                    .map(|dt| {
180                        DateTime::parse_from_rfc3339(&dt)
181                            .map_err(|_| {
182                                Error::DatabaseError("Invalid datetime format".to_string())
183                            })
184                            .map(|dt| dt.with_timezone(&Utc))
185                    })
186                    .transpose()?,
187            };
188            migrations.push(migration);
189        }
190
191        Ok(migrations)
192        }
193    }
194
195    /// Execute a migration
196    pub async fn execute_migration(&self, migration: &Migration) -> Result<(), Error> {
197        // Begin transaction
198        self.db
199            .execute("BEGIN", vec![null_value(); 0])
200            .await?;
201
202        // Execute the migration SQL
203        self.db
204            .execute(&migration.sql, vec![null_value(); 0])
205            .await?;
206
207        // Record the migration
208        let sql = r#"
209            INSERT INTO migrations (id, name, sql, created_at, executed_at)
210            VALUES (?, ?, ?, ?, ?)
211        "#;
212
213        self.db
214            .execute(
215                sql,
216                vec![
217                    text_value(migration.id.clone()),
218                    text_value(migration.name.clone()),
219                    text_value(migration.sql.clone()),
220                    text_value(migration.created_at.to_rfc3339()),
221                    text_value(Utc::now().to_rfc3339()),
222                ],
223            )
224            .await?;
225
226        // Commit transaction
227        self.db
228            .execute("COMMIT", vec![null_value(); 0])
229            .await?;
230
231        Ok(())
232    }
233
234    /// Rollback a migration
235    pub async fn rollback_migration(&self, migration_id: &str) -> Result<(), Error> {
236        let sql = "DELETE FROM migrations WHERE id = ?";
237        self.db
238            .execute(sql, vec![text_value(migration_id.to_string())])
239            .await?;
240        Ok(())
241    }
242
243    /// Get pending migrations (not yet executed)
244    pub async fn get_pending_migrations(&self) -> Result<Vec<Migration>, Error> {
245        let migrations = self.get_migrations().await?;
246        Ok(migrations
247            .into_iter()
248            .filter(|m| m.executed_at.is_none())
249            .collect())
250    }
251
252    /// Get executed migrations
253    pub async fn get_executed_migrations(&self) -> Result<Vec<Migration>, Error> {
254        let migrations = self.get_migrations().await?;
255        Ok(migrations
256            .into_iter()
257            .filter(|m| m.executed_at.is_some())
258            .collect())
259    }
260
261    /// Run all pending migrations
262    pub async fn run_migrations(&self, migrations: Vec<Migration>) -> Result<(), Error> {
263        for migration in migrations {
264            if let Some(_executed_at) = migration.executed_at {
265                continue;
266            }
267
268            self.execute_migration(&migration).await?;
269        }
270
271        Ok(())
272    }
273
274    /// Create a migration from a file
275    pub async fn create_migration_from_file(
276        name: &str,
277        file_path: &str,
278    ) -> Result<Migration, Error> {
279        let sql = std::fs::read_to_string(file_path)
280            .map_err(|e| Error::DatabaseError(format!("Failed to read migration file: {e}")))?;
281
282        Ok(Self::create_migration(name, &sql))
283    }
284
285    /// Generate a migration name from a description
286    pub fn generate_migration_name(description: &str) -> String {
287        let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
288        let sanitized_description = description
289            .to_lowercase()
290            .replace(" ", "_")
291            .replace("-", "_")
292            .chars()
293            .filter(|c| c.is_alphanumeric() || *c == '_')
294            .collect::<String>();
295
296        format!("{timestamp}_{sanitized_description}")
297    }
298
299    pub fn database(&self) -> &Database {
300        &self.db
301    }
302}
303
304/// Builder for creating migrations
305///
306/// Provides a fluent interface for constructing migrations with up and down SQL.
307///
308/// # Examples
309///
310/// ```rust
311/// use libsql_orm::MigrationBuilder;
312///
313/// let migration = MigrationBuilder::new("add_user_email_index")
314///     .up("CREATE UNIQUE INDEX idx_users_email ON users(email)")
315///     .down("DROP INDEX idx_users_email")
316///     .build();
317/// ```
318pub struct MigrationBuilder {
319    name: String,
320    up_sql: String,
321    down_sql: Option<String>,
322}
323
324impl MigrationBuilder {
325    /// Create a new migration builder
326    pub fn new(name: &str) -> Self {
327        Self {
328            name: name.to_string(),
329            up_sql: String::new(),
330            down_sql: None,
331        }
332    }
333
334    /// Add SQL for the up migration
335    pub fn up(mut self, sql: &str) -> Self {
336        self.up_sql = sql.to_string();
337        self
338    }
339
340    /// Add SQL for the down migration (rollback)
341    pub fn down(mut self, sql: &str) -> Self {
342        self.down_sql = Some(sql.to_string());
343        self
344    }
345
346    /// Build the migration
347    pub fn build(self) -> Migration {
348        Migration {
349            id: uuid::Uuid::new_v4().to_string(),
350            name: self.name,
351            sql: self.up_sql,
352            created_at: Utc::now(),
353            executed_at: None,
354        }
355    }
356}
357
358/// Common migration templates
359///
360/// Pre-built migration templates for common database operations like creating tables,
361/// adding columns, creating indexes, etc. These templates provide a quick way to
362/// generate migrations for standard operations.
363///
364/// # Examples
365///
366/// ```rust
367/// use libsql_orm::templates;
368///
369/// // Create a new table
370/// let create_table = templates::create_table("users", &[
371///     ("id", "INTEGER PRIMARY KEY AUTOINCREMENT"),
372///     ("name", "TEXT NOT NULL"),
373///     ("email", "TEXT UNIQUE NOT NULL"),
374/// ]);
375///
376/// // Add a column to existing table
377/// let add_column = templates::add_column("users", "created_at", "TEXT NOT NULL");
378///
379/// // Create an index
380/// let create_index = templates::create_index("idx_users_email", "users", &["email"]);
381/// ```
382pub mod templates {
383    use super::*;
384
385    /// Create a table migration
386    pub fn create_table(table_name: &str, columns: &[(&str, &str)]) -> Migration {
387        let column_definitions = columns
388            .iter()
389            .map(|(name, definition)| format!("{name} {definition}"))
390            .collect::<Vec<_>>()
391            .join(", ");
392
393        let sql = format!("CREATE TABLE {table_name} ({column_definitions})");
394
395        MigrationBuilder::new(&format!("create_table_{table_name}"))
396            .up(&sql)
397            .build()
398    }
399
400    /// Add column migration
401    pub fn add_column(table_name: &str, column_name: &str, definition: &str) -> Migration {
402        let sql = format!("ALTER TABLE {table_name} ADD COLUMN {column_name} {definition}");
403
404        MigrationBuilder::new(&format!("add_column_{table_name}_{column_name}"))
405            .up(&sql)
406            .build()
407    }
408
409    /// Drop column migration
410    pub fn drop_column(table_name: &str, column_name: &str) -> Migration {
411        let sql = format!("ALTER TABLE {table_name} DROP COLUMN {column_name}");
412
413        MigrationBuilder::new(&format!("drop_column_{table_name}_{column_name}"))
414            .up(&sql)
415            .build()
416    }
417
418    /// Create index migration
419    pub fn create_index(index_name: &str, table_name: &str, columns: &[&str]) -> Migration {
420        let column_list = columns.join(", ");
421        let sql = format!("CREATE INDEX {index_name} ON {table_name} ({column_list})");
422
423        MigrationBuilder::new(&format!("create_index_{index_name}"))
424            .up(&sql)
425            .build()
426    }
427
428    /// Drop index migration
429    pub fn drop_index(index_name: &str) -> Migration {
430        let sql = format!("DROP INDEX {index_name}");
431
432        MigrationBuilder::new(&format!("drop_index_{index_name}"))
433            .up(&sql)
434            .build()
435    }
436}