a3s-orm 0.3.1

A type-safe, executor-neutral SQL query builder for Rust
Documentation
#![cfg(feature = "sqlite")]

use a3s_orm::{
    orm_table, select_from, Database, Migration, MigrationError, Migrator, SqliteDialect,
    SqliteExecutor,
};

orm_table! {
    struct Widget => "widget" {
        id: i64 => "id",
    }
}

orm_table! {
    struct RollbackProbe => "rollback_probe" {
        id: i64 => "id",
    }
}

fn migrations() -> Vec<Migration> {
    vec![
        Migration::new(
            "001",
            "create widgets",
            "create table widget (id integer primary key)",
        ),
        Migration::new("002", "seed widgets", "insert into widget (id) values (1)"),
    ]
}

#[tokio::test]
async fn applies_once_and_reports_up_to_date() {
    let executor = SqliteExecutor::open_in_memory().await.unwrap();
    let migrator = Migrator::new(executor.clone());
    let first = migrator.run(migrations()).await.unwrap();
    assert_eq!(first.applied, ["001", "002"]);
    let second = migrator.run(migrations()).await.unwrap();
    assert!(second.is_up_to_date());

    let database = Database::new(SqliteDialect, executor);
    let rows = database
        .fetch_all_as(select_from::<Widget>().select(Widget::id()))
        .await
        .unwrap()
        .rows;
    assert_eq!(rows, vec![1]);
}

#[tokio::test]
async fn serving_admission_reads_the_ledger_without_creating_it() {
    let executor = SqliteExecutor::open_in_memory().await.unwrap();
    let migrator = Migrator::new(executor);

    let absent = migrator.verify_required(migrations()).await.unwrap_err();
    assert!(matches!(
        absent,
        a3s_orm::migration::MigrationRunError::Backend(a3s_orm::SqliteMigrationError::Driver(_))
    ));

    let applied = migrator.run(migrations()).await.unwrap();
    assert_eq!(applied.applied, ["001", "002"]);
    migrator.verify_required(migrations()).await.unwrap();
}

#[tokio::test]
async fn detects_changed_and_missing_source_migrations() {
    let executor = SqliteExecutor::open_in_memory().await.unwrap();
    let migrator = Migrator::new(executor);
    migrator.run(migrations()).await.unwrap();

    let changed = migrator
        .run([
            Migration::new(
                "001",
                "create widgets",
                "create table widget (id integer primary key, changed text)",
            ),
            migrations().remove(1),
        ])
        .await
        .unwrap_err();
    assert!(matches!(
        changed,
        a3s_orm::migration::MigrationRunError::Backend(a3s_orm::SqliteMigrationError::Migration(
            MigrationError::ChecksumMismatch { .. }
        ))
    ));

    let missing = migrator.run([migrations().remove(1)]).await.unwrap_err();
    assert!(matches!(
        missing,
        a3s_orm::migration::MigrationRunError::Backend(
            a3s_orm::SqliteMigrationError::Migration(
                MigrationError::MissingSourceMigration(version)
            )
        ) if version == "001"
    ));
}

#[tokio::test]
async fn failed_migration_rolls_back_schema_and_history() {
    let executor = SqliteExecutor::open_in_memory().await.unwrap();
    let migrator = Migrator::new(executor.clone());
    let error = migrator
        .run([Migration::new(
            "001",
            "broken migration",
            "create table rollback_probe (id integer); invalid sql",
        )])
        .await;
    let error = error.unwrap_err();
    assert!(error.to_string().contains("001"));

    let database = Database::new(SqliteDialect, executor.clone());
    assert!(database
        .fetch_all(select_from::<RollbackProbe>().select(RollbackProbe::id()))
        .await
        .is_err());

    let repaired = migrator
        .run([Migration::new(
            "001",
            "broken migration",
            "create table rollback_probe (id integer)",
        )])
        .await
        .unwrap();
    assert_eq!(repaired.applied, ["001"]);
}

#[tokio::test]
async fn concurrent_migrators_serialize_on_the_shared_connection() {
    let executor = SqliteExecutor::open_in_memory().await.unwrap();
    let left = Migrator::new(executor.clone());
    let right = Migrator::new(executor);
    let (left, right) = tokio::join!(left.run(migrations()), right.run(migrations()));
    let applied = left.unwrap().applied.len() + right.unwrap().applied.len();
    assert_eq!(applied, 2);
}