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}