slave-framework 0.6.0

Runtime for slave apps (a Laravel-inspired Rust framework for MCP-first apps): server lifecycle, config, logging, Turso Cloud database and migrations, and OpenID Connect sign-in
Documentation
//! Schema migrations for the local Turso database.
//!
//! A migration is a type implementing [`Migration`]: `up` returns the SQL that
//! applies it and `down` optionally returns SQL that reverts it. Migrations are
//! listed with the [`migrations!`](crate::migrations) macro and executed by a
//! [`Migrator`], which records each applied migration with its batch number in
//! the [`TABLE`] tracking table.
//!
//! Every migration runs inside its own transaction together with its tracking
//! row, so a failing migration leaves neither schema changes nor a record
//! behind. Turso, like SQLite, supports transactional DDL; statements that
//! cannot run in a transaction (such as `VACUUM`) must opt out through
//! [`Migration::transactional`].

pub mod console;
mod migrator;

pub use migrator::{Failure, Migrator, Outcome, RollbackReport, RunReport, State, Status, TABLE};

/// Drops every view and table except SQLite's and libSQL's own (indexes and
/// triggers go with their tables). Used by `migrate fresh` and by tests.
pub async fn drop_all_tables(conn: &libsql::Connection) -> crate::Result<usize> {
    let mut rows = conn
        .query(
            "SELECT type, name FROM sqlite_schema
             WHERE type IN ('table', 'view')
               AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'libsql_%'
               AND name NOT LIKE '_litestream%'
             ORDER BY type DESC",
            (),
        )
        .await?;
    let mut objects = Vec::new();
    while let Some(row) = rows.next().await? {
        objects.push((row.get::<String>(0)?, row.get::<String>(1)?));
    }
    drop(rows);

    conn.execute("PRAGMA foreign_keys = OFF", ()).await?;
    for (kind, name) in &objects {
        let kind = if kind == "view" { "VIEW" } else { "TABLE" };
        let name = name.replace('"', "\"\"");
        conn.execute(&format!("DROP {kind} IF EXISTS \"{name}\""), ())
            .await?;
    }
    conn.execute("PRAGMA foreign_keys = ON", ()).await?;
    Ok(objects.iter().filter(|(kind, _)| kind == "table").count())
}

/// An ordered list of SQL statements produced by a migration.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Schema {
    statements: Vec<String>,
}

impl Schema {
    pub fn new() -> Self {
        Self::default()
    }

    /// Appends a SQL statement. A string may contain several `;`-separated statements.
    #[must_use]
    pub fn execute(mut self, sql: impl Into<String>) -> Self {
        self.statements.push(sql.into());
        self
    }

    pub fn statements(&self) -> &[String] {
        &self.statements
    }
}

/// A single schema change.
pub trait Migration: Send + Sync {
    /// Statements that apply the migration.
    fn up(&self) -> Schema;

    /// Statements that revert [`up`](Migration::up), or `None` when the change
    /// cannot be reversed safely (for example, dropping a table loses its data).
    ///
    /// A batch containing an irreversible migration is never rolled back.
    fn down(&self) -> Option<Schema> {
        None
    }

    /// Whether to wrap the migration in a transaction. Defaults to `true`.
    ///
    /// Opt out only for statements that cannot run inside a transaction; if a
    /// non-transactional migration fails, earlier statements may remain applied.
    fn transactional(&self) -> bool {
        true
    }
}

/// A migration paired with its unique name, which also determines run order.
#[derive(Clone, Copy)]
pub struct Registered {
    name: &'static str,
    migration: &'static dyn Migration,
}

impl Registered {
    pub fn new(name: &'static str, migration: &'static dyn Migration) -> Self {
        Self { name, migration }
    }

    /// Registers a migration from a module named `m<name>`, as produced by
    /// `slave make:migration` (`m20261016160000_create_users_table`).
    pub fn from_module(module: &'static str, migration: &'static dyn Migration) -> Self {
        Self::new(module.strip_prefix('m').unwrap_or(module), migration)
    }

    pub fn name(&self) -> &'static str {
        self.name
    }

    pub fn migration(&self) -> &'static dyn Migration {
        self.migration
    }
}

impl std::fmt::Debug for Registered {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Registered")
            .field("name", &self.name)
            .finish()
    }
}

/// Lists migration modules, each exposing `pub const MIGRATION: &dyn Migration`.
///
/// ```ignore
/// slave_framework::migrations![m20261016160000_create_users_table]
/// ```
#[macro_export]
macro_rules! migrations {
    ($($module:ident),* $(,)?) => {
        vec![$(
            $crate::migration::Registered::from_module(stringify!($module), $module::MIGRATION)
        ),*]
    };
}