libsql-orm 0.2.5

A powerful, async-first ORM for Turso Database with first-class support for Cloudflare Workers and WebAssembly environments. Features include automatic boolean conversion, upsert operations, built-in logging, migrations, and comprehensive query building.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
//! Database migration system for libsql-orm
//!
//! This module provides a comprehensive migration system for managing database schema
//! changes over time. It supports creating, executing, and tracking migrations with
//! both manual and auto-generated approaches.
//!
//! # Features
//!
//! - **Auto-generation**: Generate migrations from model definitions
//! - **Manual creation**: Build custom migrations with the builder pattern
//! - **Templates**: Pre-built migration templates for common operations
//! - **History tracking**: Track which migrations have been executed
//! - **Rollback support**: Reverse migrations with down scripts
//! - **Batch execution**: Run multiple migrations in sequence
//!
//! # Basic Usage
//!
//! ```no_run
//! use libsql_orm::{MigrationManager, MigrationBuilder, generate_migration, Database, Error, Model};
//! # #[derive(libsql_orm::Model, Clone, serde::Serialize, serde::Deserialize)]
//! # struct User { id: Option<i64>, name: String }
//!
//! async fn run_migrations(db: Database) -> Result<(), Error> {
//!     let manager = MigrationManager::new(db);
//!     manager.init().await?;
//!
//!     // Auto-generate from model
//!     let migration = generate_migration!(User);
//!     manager.execute_migration(&migration).await?;
//!
//!     // Manual migration
//!     let manual_migration = MigrationBuilder::new("add_index")
//!         .up("CREATE INDEX idx_users_email ON users(email)")
//!         .down("DROP INDEX idx_users_email")
//!         .build();
//!
//!     manager.execute_migration(&manual_migration).await?;
//!     Ok(())
//! }
//! ```
//!
//! # Migration Templates
//!
//! ```rust
//! use libsql_orm::templates;
//!
//! // Create table
//! let create_table = templates::create_table("posts", &[
//!     ("id", "INTEGER PRIMARY KEY AUTOINCREMENT"),
//!     ("title", "TEXT NOT NULL"),
//!     ("content", "TEXT"),
//! ]);
//!
//! // Add column
//! let add_column = templates::add_column("posts", "published_at", "TEXT");
//!
//! // Create index
//! let create_index = templates::create_index("idx_posts_title", "posts", &["title"]);
//! ```

use crate::{compat::text_value, database::Database, error::Error};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// Represents a database migration
///
/// A migration contains the SQL statements needed to evolve the database schema
/// along with metadata for tracking execution history.
///
/// # Examples
///
/// ```rust
/// use libsql_orm::{Migration, MigrationBuilder};
///
/// let migration = MigrationBuilder::new("create_users_table")
///     .up("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)")
///     .down("DROP TABLE users")
///     .build();
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Migration {
    pub id: String,
    pub name: String,
    pub sql: String,
    pub created_at: DateTime<Utc>,
    pub executed_at: Option<DateTime<Utc>>,
}

/// Migration manager for handling database schema changes
///
/// The central component for managing database migrations. Handles initialization,
/// execution, and tracking of schema changes.
///
/// # Examples
///
/// ```no_run
/// use libsql_orm::{MigrationManager, Database, Error};
///
/// async fn setup_migrations(db: Database) -> Result<(), Error> {
///     let manager = MigrationManager::new(db);
///
///     // Initialize migration tracking
///     manager.init().await?;
///
///     // Get migration status
///     let executed = manager.get_executed_migrations().await?;
///     let pending = manager.get_pending_migrations().await?;
///
///     println!("Executed: {}, Pending: {}", executed.len(), pending.len());
///     Ok(())
/// }
/// ```
pub struct MigrationManager {
    db: Database,
}

impl MigrationManager {
    /// Create a new migration manager
    pub fn new(db: Database) -> Self {
        Self { db }
    }

    /// Initialize the migration table
    pub async fn init(&self) -> Result<(), Error> {
        let sql = r#"
            CREATE TABLE IF NOT EXISTS migrations (
                id TEXT PRIMARY KEY,
                name TEXT NOT NULL,
                sql TEXT NOT NULL,
                created_at TEXT NOT NULL,
                executed_at TEXT
            )
        "#;

        let params = vec![];

        self.db.execute(sql, params).await?;
        Ok(())
    }

    /// Create a new migration
    pub fn create_migration(name: &str, sql: &str) -> Migration {
        Migration {
            id: uuid::Uuid::new_v4().to_string(),
            name: name.to_string(),
            sql: sql.to_string(),
            created_at: Utc::now(),
            executed_at: None,
        }
    }

    /// Get all migrations from the database
    pub async fn get_migrations(&self) -> Result<Vec<Migration>, Error> {
        #[cfg(not(feature = "libsql"))]
        {
            // Return empty migrations for WASM-only builds
            return Ok(vec![]);
        }

        #[cfg(feature = "libsql")]
        {
            let sql =
                "SELECT id, name, sql, created_at, executed_at FROM migrations ORDER BY created_at";
            let mut rows = self.db.query(sql, vec![]).await?;

            let mut migrations = Vec::new();
            while let Some(row) = rows.next().await? {
                let migration = Migration {
                    id: row.get(0)?,
                    name: row.get(1)?,
                    sql: row.get(2)?,
                    created_at: DateTime::parse_from_rfc3339(
                        &row.get::<String>(3).unwrap_or_default(),
                    )
                    .map_err(|_| Error::DatabaseError("Invalid datetime format".to_string()))?
                    .with_timezone(&Utc),
                    executed_at: row
                        .get::<Option<String>>(4)
                        .unwrap_or(None)
                        .map(|dt| {
                            DateTime::parse_from_rfc3339(&dt)
                                .map_err(|_| {
                                    Error::DatabaseError("Invalid datetime format".to_string())
                                })
                                .map(|dt| dt.with_timezone(&Utc))
                        })
                        .transpose()?,
                };
                migrations.push(migration);
            }

            Ok(migrations)
        }
    }

    /// Execute a migration
    pub async fn execute_migration(&self, migration: &Migration) -> Result<(), Error> {
        // Begin transaction
        self.db.execute("BEGIN", vec![]).await?;

        // Execute the migration SQL
        self.db.execute(&migration.sql, vec![]).await?;

        // Record the migration
        let sql = r#"
            INSERT INTO migrations (id, name, sql, created_at, executed_at)
            VALUES (?, ?, ?, ?, ?)
        "#;

        self.db
            .execute(
                sql,
                vec![
                    text_value(migration.id.clone()),
                    text_value(migration.name.clone()),
                    text_value(migration.sql.clone()),
                    text_value(migration.created_at.to_rfc3339()),
                    text_value(Utc::now().to_rfc3339()),
                ],
            )
            .await?;

        // Commit transaction
        self.db.execute("COMMIT", vec![]).await?;

        Ok(())
    }

    /// Rollback a migration
    pub async fn rollback_migration(&self, migration_id: &str) -> Result<(), Error> {
        let sql = "DELETE FROM migrations WHERE id = ?";
        self.db
            .execute(sql, vec![text_value(migration_id.to_string())])
            .await?;
        Ok(())
    }

    /// Get pending migrations (not yet executed)
    pub async fn get_pending_migrations(&self) -> Result<Vec<Migration>, Error> {
        let migrations = self.get_migrations().await?;
        Ok(migrations
            .into_iter()
            .filter(|m| m.executed_at.is_none())
            .collect())
    }

    /// Get executed migrations
    pub async fn get_executed_migrations(&self) -> Result<Vec<Migration>, Error> {
        let migrations = self.get_migrations().await?;
        Ok(migrations
            .into_iter()
            .filter(|m| m.executed_at.is_some())
            .collect())
    }

    /// Run all pending migrations
    pub async fn run_migrations(&self, migrations: Vec<Migration>) -> Result<(), Error> {
        for migration in migrations {
            if let Some(_executed_at) = migration.executed_at {
                continue;
            }

            self.execute_migration(&migration).await?;
        }

        Ok(())
    }

    /// Create a migration from a file
    pub async fn create_migration_from_file(
        name: &str,
        file_path: &str,
    ) -> Result<Migration, Error> {
        let sql = std::fs::read_to_string(file_path)
            .map_err(|e| Error::DatabaseError(format!("Failed to read migration file: {e}")))?;

        Ok(Self::create_migration(name, &sql))
    }

    /// Generate a migration name from a description
    pub fn generate_migration_name(description: &str) -> String {
        let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
        let sanitized_description = description
            .to_lowercase()
            .replace(" ", "_")
            .replace("-", "_")
            .chars()
            .filter(|c| c.is_alphanumeric() || *c == '_')
            .collect::<String>();

        format!("{timestamp}_{sanitized_description}")
    }

    pub fn database(&self) -> &Database {
        &self.db
    }
}

/// Builder for creating migrations
///
/// Provides a fluent interface for constructing migrations with up and down SQL.
///
/// # Examples
///
/// ```rust
/// use libsql_orm::MigrationBuilder;
///
/// let migration = MigrationBuilder::new("add_user_email_index")
///     .up("CREATE UNIQUE INDEX idx_users_email ON users(email)")
///     .down("DROP INDEX idx_users_email")
///     .build();
/// ```
pub struct MigrationBuilder {
    name: String,
    up_sql: String,
    down_sql: Option<String>,
}

impl MigrationBuilder {
    /// Create a new migration builder
    pub fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
            up_sql: String::new(),
            down_sql: None,
        }
    }

    /// Add SQL for the up migration
    pub fn up(mut self, sql: &str) -> Self {
        self.up_sql = sql.to_string();
        self
    }

    /// Add SQL for the down migration (rollback)
    pub fn down(mut self, sql: &str) -> Self {
        self.down_sql = Some(sql.to_string());
        self
    }

    /// Build the migration
    pub fn build(self) -> Migration {
        Migration {
            id: uuid::Uuid::new_v4().to_string(),
            name: self.name,
            sql: self.up_sql,
            created_at: Utc::now(),
            executed_at: None,
        }
    }
}

/// Common migration templates
///
/// Pre-built migration templates for common database operations like creating tables,
/// adding columns, creating indexes, etc. These templates provide a quick way to
/// generate migrations for standard operations.
///
/// # Examples
///
/// ```rust
/// use libsql_orm::templates;
///
/// // Create a new table
/// let create_table = templates::create_table("users", &[
///     ("id", "INTEGER PRIMARY KEY AUTOINCREMENT"),
///     ("name", "TEXT NOT NULL"),
///     ("email", "TEXT UNIQUE NOT NULL"),
/// ]);
///
/// // Add a column to existing table
/// let add_column = templates::add_column("users", "created_at", "TEXT NOT NULL");
///
/// // Create an index
/// let create_index = templates::create_index("idx_users_email", "users", &["email"]);
/// ```
pub mod templates {
    use super::*;

    /// Create a table migration
    pub fn create_table(table_name: &str, columns: &[(&str, &str)]) -> Migration {
        let column_definitions = columns
            .iter()
            .map(|(name, definition)| format!("{name} {definition}"))
            .collect::<Vec<_>>()
            .join(", ");

        let sql = format!("CREATE TABLE {table_name} ({column_definitions})");

        MigrationBuilder::new(&format!("create_table_{table_name}"))
            .up(&sql)
            .build()
    }

    /// Add column migration
    pub fn add_column(table_name: &str, column_name: &str, definition: &str) -> Migration {
        let sql = format!("ALTER TABLE {table_name} ADD COLUMN {column_name} {definition}");

        MigrationBuilder::new(&format!("add_column_{table_name}_{column_name}"))
            .up(&sql)
            .build()
    }

    /// Drop column migration
    pub fn drop_column(table_name: &str, column_name: &str) -> Migration {
        let sql = format!("ALTER TABLE {table_name} DROP COLUMN {column_name}");

        MigrationBuilder::new(&format!("drop_column_{table_name}_{column_name}"))
            .up(&sql)
            .build()
    }

    /// Create index migration
    pub fn create_index(index_name: &str, table_name: &str, columns: &[&str]) -> Migration {
        let column_list = columns.join(", ");
        let sql = format!("CREATE INDEX {index_name} ON {table_name} ({column_list})");

        MigrationBuilder::new(&format!("create_index_{index_name}"))
            .up(&sql)
            .build()
    }

    /// Drop index migration
    pub fn drop_index(index_name: &str) -> Migration {
        let sql = format!("DROP INDEX {index_name}");

        MigrationBuilder::new(&format!("drop_index_{index_name}"))
            .up(&sql)
            .build()
    }
}