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