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}