a3s-orm 0.3.1

A type-safe, executor-neutral SQL query builder for Rust
Documentation
use std::sync::Mutex;

use a3s_orm::{
    AppliedMigration, Migration, MigrationBackend, MigrationError, MigrationLedger,
    MigrationReport, Migrator, PreparedMigration,
};
use async_trait::async_trait;

#[derive(Default)]
struct RecordingBackend {
    migrations: Mutex<Vec<PreparedMigration>>,
    applied: Mutex<Vec<AppliedMigration>>,
}

#[derive(Debug, thiserror::Error)]
#[error("recording backend failed")]
struct RecordingError;

#[async_trait]
impl MigrationBackend for RecordingBackend {
    type Error = RecordingError;

    async fn apply(
        &self,
        migrations: &[PreparedMigration],
    ) -> Result<MigrationReport, Self::Error> {
        self.migrations
            .lock()
            .unwrap()
            .extend_from_slice(migrations);
        Ok(MigrationReport {
            applied: migrations
                .iter()
                .map(|migration| migration.version().to_owned())
                .collect(),
        })
    }
}

#[async_trait]
impl MigrationLedger for RecordingBackend {
    type Error = RecordingError;

    async fn applied_migrations(&self) -> Result<Vec<AppliedMigration>, Self::Error> {
        Ok(self.applied.lock().unwrap().clone())
    }
}

#[tokio::test]
async fn validates_sorts_and_checksums_migrations_before_backend_execution() {
    let migrator = Migrator::new(RecordingBackend::default());
    let report = migrator
        .run([
            Migration::new("002", "second", "create table second (id integer)"),
            Migration::new("001", "first", "create table first (id integer)"),
        ])
        .await
        .unwrap();
    assert_eq!(report.applied, ["001", "002"]);
    let recorded = migrator.backend().migrations.lock().unwrap();
    assert_eq!(recorded[0].version(), "001");
    assert_eq!(recorded[0].checksum().len(), 64);
    assert_eq!(
        recorded[0].checksum(),
        "10e0a2282e051aa0de3c957c7ef36d748cea0a244fed443abb5917b55d7a8384"
    );
    assert_ne!(recorded[0].checksum(), recorded[1].checksum());
}

#[tokio::test]
async fn rejects_invalid_and_duplicate_versions_without_calling_backend() {
    let migrator = Migrator::new(RecordingBackend::default());
    let error = migrator
        .run([
            Migration::new("001", "first", "select 1"),
            Migration::new("001", "duplicate", "select 2"),
        ])
        .await
        .unwrap_err();
    assert!(matches!(
        error,
        a3s_orm::migration::MigrationRunError::Validation(
            MigrationError::DuplicateVersion(version)
        ) if version == "001"
    ));
    assert!(migrator.backend().migrations.lock().unwrap().is_empty());
}

#[tokio::test]
async fn verifies_required_migrations_without_applying_or_rejecting_a_compatible_superset() {
    let required = Migration::new("001", "first", "select 1");
    let checksum = {
        let recorder = Migrator::new(RecordingBackend::default());
        recorder.run([required.clone()]).await.unwrap();
        let checksum = recorder.backend().migrations.lock().unwrap()[0]
            .checksum()
            .to_owned();
        checksum
    };
    let backend = RecordingBackend {
        applied: Mutex::new(vec![
            AppliedMigration {
                version: "001".into(),
                checksum,
            },
            AppliedMigration {
                version: "002".into(),
                checksum: "a later migration is outside this binary's required set".into(),
            },
        ]),
        ..RecordingBackend::default()
    };
    let migrator = Migrator::new(backend);

    migrator.verify_required([required]).await.unwrap();
    assert!(migrator.backend().migrations.lock().unwrap().is_empty());
}

#[tokio::test]
async fn rejects_missing_or_changed_required_migrations() {
    let missing = Migrator::new(RecordingBackend::default())
        .verify_required([Migration::new("001", "first", "select 1")])
        .await
        .unwrap_err();
    assert!(matches!(
        missing,
        a3s_orm::migration::MigrationRunError::Validation(
            MigrationError::MissingAppliedMigration(version)
        ) if version == "001"
    ));

    let changed = Migrator::new(RecordingBackend {
        applied: Mutex::new(vec![AppliedMigration {
            version: "001".into(),
            checksum: "wrong".into(),
        }]),
        ..RecordingBackend::default()
    })
    .verify_required([Migration::new("001", "first", "select 1")])
    .await
    .unwrap_err();
    assert!(matches!(
        changed,
        a3s_orm::migration::MigrationRunError::Validation(
            MigrationError::ChecksumMismatch { version, .. }
        ) if version == "001"
    ));
}