Skip to main content

a3s_orm/migration/
definition.rs

1use sha2::{Digest, Sha256};
2
3use super::MigrationError;
4
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct Migration {
7    version: String,
8    name: String,
9    up_sql: String,
10}
11
12impl Migration {
13    pub fn new(
14        version: impl Into<String>,
15        name: impl Into<String>,
16        up_sql: impl Into<String>,
17    ) -> Self {
18        Self {
19            version: version.into(),
20            name: name.into(),
21            up_sql: up_sql.into(),
22        }
23    }
24
25    pub fn version(&self) -> &str {
26        &self.version
27    }
28
29    pub fn name(&self) -> &str {
30        &self.name
31    }
32
33    pub fn up_sql(&self) -> &str {
34        &self.up_sql
35    }
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct PreparedMigration {
40    version: String,
41    name: String,
42    up_sql: String,
43    checksum: String,
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct AppliedMigration {
48    pub version: String,
49    pub checksum: String,
50}
51
52impl PreparedMigration {
53    pub(crate) fn prepare(migration: Migration) -> Self {
54        let mut digest = Sha256::new();
55        digest.update(migration.up_sql.as_bytes());
56        let checksum = hex_encode(&digest.finalize());
57        Self {
58            version: migration.version,
59            name: migration.name,
60            up_sql: migration.up_sql,
61            checksum,
62        }
63    }
64
65    pub fn version(&self) -> &str {
66        &self.version
67    }
68
69    pub fn name(&self) -> &str {
70        &self.name
71    }
72
73    pub fn up_sql(&self) -> &str {
74        &self.up_sql
75    }
76
77    pub fn checksum(&self) -> &str {
78        &self.checksum
79    }
80}
81
82fn hex_encode(bytes: &[u8]) -> String {
83    const HEX: &[u8; 16] = b"0123456789abcdef";
84    let mut encoded = String::with_capacity(bytes.len() * 2);
85    for byte in bytes {
86        encoded.push(HEX[(byte >> 4) as usize] as char);
87        encoded.push(HEX[(byte & 0x0f) as usize] as char);
88    }
89    encoded
90}
91
92pub fn pending_migrations<'a>(
93    applied: &[AppliedMigration],
94    source: &'a [PreparedMigration],
95) -> Result<Vec<&'a PreparedMigration>, MigrationError> {
96    for applied_migration in applied {
97        let Some(source_migration) = source
98            .iter()
99            .find(|migration| migration.version() == applied_migration.version)
100        else {
101            return Err(MigrationError::MissingSourceMigration(
102                applied_migration.version.clone(),
103            ));
104        };
105        if source_migration.checksum() != applied_migration.checksum {
106            return Err(MigrationError::ChecksumMismatch {
107                version: applied_migration.version.clone(),
108                applied_checksum: applied_migration.checksum.clone(),
109                source_checksum: source_migration.checksum().to_owned(),
110            });
111        }
112    }
113    Ok(source
114        .iter()
115        .filter(|migration| {
116            !applied
117                .iter()
118                .any(|applied| applied.version == migration.version())
119        })
120        .collect())
121}