a3s-orm 0.2.1

A type-safe, executor-neutral SQL query builder for Rust
Documentation
use sha2::{Digest, Sha256};

use super::MigrationError;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Migration {
    version: String,
    name: String,
    up_sql: String,
}

impl Migration {
    pub fn new(
        version: impl Into<String>,
        name: impl Into<String>,
        up_sql: impl Into<String>,
    ) -> Self {
        Self {
            version: version.into(),
            name: name.into(),
            up_sql: up_sql.into(),
        }
    }

    pub fn version(&self) -> &str {
        &self.version
    }

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

    pub fn up_sql(&self) -> &str {
        &self.up_sql
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PreparedMigration {
    version: String,
    name: String,
    up_sql: String,
    checksum: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AppliedMigration {
    pub version: String,
    pub checksum: String,
}

impl PreparedMigration {
    pub(crate) fn prepare(migration: Migration) -> Self {
        let mut digest = Sha256::new();
        digest.update(migration.up_sql.as_bytes());
        let checksum = hex_encode(&digest.finalize());
        Self {
            version: migration.version,
            name: migration.name,
            up_sql: migration.up_sql,
            checksum,
        }
    }

    pub fn version(&self) -> &str {
        &self.version
    }

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

    pub fn up_sql(&self) -> &str {
        &self.up_sql
    }

    pub fn checksum(&self) -> &str {
        &self.checksum
    }
}

fn hex_encode(bytes: &[u8]) -> String {
    const HEX: &[u8; 16] = b"0123456789abcdef";
    let mut encoded = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        encoded.push(HEX[(byte >> 4) as usize] as char);
        encoded.push(HEX[(byte & 0x0f) as usize] as char);
    }
    encoded
}

pub fn pending_migrations<'a>(
    applied: &[AppliedMigration],
    source: &'a [PreparedMigration],
) -> Result<Vec<&'a PreparedMigration>, MigrationError> {
    for applied_migration in applied {
        let Some(source_migration) = source
            .iter()
            .find(|migration| migration.version() == applied_migration.version)
        else {
            return Err(MigrationError::MissingSourceMigration(
                applied_migration.version.clone(),
            ));
        };
        if source_migration.checksum() != applied_migration.checksum {
            return Err(MigrationError::ChecksumMismatch {
                version: applied_migration.version.clone(),
                applied_checksum: applied_migration.checksum.clone(),
                source_checksum: source_migration.checksum().to_owned(),
            });
        }
    }
    Ok(source
        .iter()
        .filter(|migration| {
            !applied
                .iter()
                .any(|applied| applied.version == migration.version())
        })
        .collect())
}