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}