Skip to main content

slave_framework/migration/
mod.rs

1//! Schema migrations for the local Turso database.
2//!
3//! A migration is a type implementing [`Migration`]: `up` returns the SQL that
4//! applies it and `down` optionally returns SQL that reverts it. Migrations are
5//! listed with the [`migrations!`](crate::migrations) macro and executed by a
6//! [`Migrator`], which records each applied migration with its batch number in
7//! the [`TABLE`] tracking table.
8//!
9//! Every migration runs inside its own transaction together with its tracking
10//! row, so a failing migration leaves neither schema changes nor a record
11//! behind. Turso, like SQLite, supports transactional DDL; statements that
12//! cannot run in a transaction (such as `VACUUM`) must opt out through
13//! [`Migration::transactional`].
14
15pub mod console;
16mod migrator;
17
18pub use migrator::{Failure, Migrator, Outcome, RollbackReport, RunReport, State, Status, TABLE};
19
20/// Drops every view and table except SQLite's and libSQL's own (indexes and
21/// triggers go with their tables). Used by `migrate fresh` and by tests.
22pub async fn drop_all_tables(conn: &libsql::Connection) -> crate::Result<usize> {
23    let mut rows = conn
24        .query(
25            "SELECT type, name FROM sqlite_schema
26             WHERE type IN ('table', 'view')
27               AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'libsql_%'
28               AND name NOT LIKE '_litestream%'
29             ORDER BY type DESC",
30            (),
31        )
32        .await?;
33    let mut objects = Vec::new();
34    while let Some(row) = rows.next().await? {
35        objects.push((row.get::<String>(0)?, row.get::<String>(1)?));
36    }
37    drop(rows);
38
39    conn.execute("PRAGMA foreign_keys = OFF", ()).await?;
40    for (kind, name) in &objects {
41        let kind = if kind == "view" { "VIEW" } else { "TABLE" };
42        let name = name.replace('"', "\"\"");
43        conn.execute(&format!("DROP {kind} IF EXISTS \"{name}\""), ())
44            .await?;
45    }
46    conn.execute("PRAGMA foreign_keys = ON", ()).await?;
47    Ok(objects.iter().filter(|(kind, _)| kind == "table").count())
48}
49
50/// An ordered list of SQL statements produced by a migration.
51#[derive(Debug, Clone, Default, PartialEq, Eq)]
52pub struct Schema {
53    statements: Vec<String>,
54}
55
56impl Schema {
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    /// Appends a SQL statement. A string may contain several `;`-separated statements.
62    #[must_use]
63    pub fn execute(mut self, sql: impl Into<String>) -> Self {
64        self.statements.push(sql.into());
65        self
66    }
67
68    pub fn statements(&self) -> &[String] {
69        &self.statements
70    }
71}
72
73/// A single schema change.
74pub trait Migration: Send + Sync {
75    /// Statements that apply the migration.
76    fn up(&self) -> Schema;
77
78    /// Statements that revert [`up`](Migration::up), or `None` when the change
79    /// cannot be reversed safely (for example, dropping a table loses its data).
80    ///
81    /// A batch containing an irreversible migration is never rolled back.
82    fn down(&self) -> Option<Schema> {
83        None
84    }
85
86    /// Whether to wrap the migration in a transaction. Defaults to `true`.
87    ///
88    /// Opt out only for statements that cannot run inside a transaction; if a
89    /// non-transactional migration fails, earlier statements may remain applied.
90    fn transactional(&self) -> bool {
91        true
92    }
93}
94
95/// A migration paired with its unique name, which also determines run order.
96#[derive(Clone, Copy)]
97pub struct Registered {
98    name: &'static str,
99    migration: &'static dyn Migration,
100}
101
102impl Registered {
103    pub fn new(name: &'static str, migration: &'static dyn Migration) -> Self {
104        Self { name, migration }
105    }
106
107    /// Registers a migration from a module named `m<name>`, as produced by
108    /// `slave make:migration` (`m20261016160000_create_users_table`).
109    pub fn from_module(module: &'static str, migration: &'static dyn Migration) -> Self {
110        Self::new(module.strip_prefix('m').unwrap_or(module), migration)
111    }
112
113    pub fn name(&self) -> &'static str {
114        self.name
115    }
116
117    pub fn migration(&self) -> &'static dyn Migration {
118        self.migration
119    }
120}
121
122impl std::fmt::Debug for Registered {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        f.debug_struct("Registered")
125            .field("name", &self.name)
126            .finish()
127    }
128}
129
130/// Lists migration modules, each exposing `pub const MIGRATION: &dyn Migration`.
131///
132/// ```ignore
133/// slave_framework::migrations![m20261016160000_create_users_table]
134/// ```
135#[macro_export]
136macro_rules! migrations {
137    ($($module:ident),* $(,)?) => {
138        vec![$(
139            $crate::migration::Registered::from_module(stringify!($module), $module::MIGRATION)
140        ),*]
141    };
142}