Skip to main content

dovecote_sqlx_sqlite/
migration.rs

1//! Versioned SQLite migration metadata.
2
3pub const SCHEMA_VERSION: u32 = 1;
4
5#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
6pub struct CrateVersion {
7    major: u16,
8    minor: u16,
9    patch: u16,
10}
11
12impl CrateVersion {
13    pub const fn new(major: u16, minor: u16, patch: u16) -> Self {
14        Self {
15            major,
16            minor,
17            patch,
18        }
19    }
20    pub const fn major(self) -> u16 {
21        self.major
22    }
23    pub const fn minor(self) -> u16 {
24        self.minor
25    }
26    pub const fn patch(self) -> u16 {
27        self.patch
28    }
29    const fn is_less_than(self, other: Self) -> bool {
30        self.major < other.major
31            || (self.major == other.major
32                && (self.minor < other.minor
33                    || (self.minor == other.minor && self.patch < other.patch)))
34    }
35}
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub struct MigrationCompatibilityError;
39impl std::fmt::Display for MigrationCompatibilityError {
40    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        formatter.write_str("migration compatibility maximum precedes minimum")
42    }
43}
44impl std::error::Error for MigrationCompatibilityError {}
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub struct MigrationCompatibility {
48    minimum: CrateVersion,
49    maximum: Option<CrateVersion>,
50}
51impl MigrationCompatibility {
52    const fn new(minimum: CrateVersion, maximum: Option<CrateVersion>) -> Self {
53        Self { minimum, maximum }
54    }
55    pub const fn try_new(
56        minimum: CrateVersion,
57        maximum: Option<CrateVersion>,
58    ) -> Result<Self, MigrationCompatibilityError> {
59        if let Some(maximum) = maximum
60            && maximum.is_less_than(minimum)
61        {
62            return Err(MigrationCompatibilityError);
63        }
64        Ok(Self::new(minimum, maximum))
65    }
66    pub const fn minimum(self) -> CrateVersion {
67        self.minimum
68    }
69    pub const fn maximum(self) -> Option<CrateVersion> {
70        self.maximum
71    }
72    pub const fn contains(self, version: CrateVersion) -> bool {
73        !version.is_less_than(self.minimum)
74            && match self.maximum {
75                Some(maximum) => !maximum.is_less_than(version),
76                None => true,
77            }
78    }
79}
80
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82pub struct Migration {
83    version: u32,
84    sql: &'static str,
85    compatibility: MigrationCompatibility,
86    rolling_compatible: bool,
87}
88impl Migration {
89    const fn new(
90        version: u32,
91        sql: &'static str,
92        compatibility: MigrationCompatibility,
93        rolling_compatible: bool,
94    ) -> Self {
95        Self {
96            version,
97            sql,
98            compatibility,
99            rolling_compatible,
100        }
101    }
102    pub const fn version(self) -> u32 {
103        self.version
104    }
105    pub const fn sql(self) -> &'static str {
106        self.sql
107    }
108    pub const fn compatibility(self) -> MigrationCompatibility {
109        self.compatibility
110    }
111    pub const fn rolling_compatible(self) -> bool {
112        self.rolling_compatible
113    }
114}
115
116pub const MIGRATIONS: &[Migration] = &[Migration::new(
117    1,
118    include_str!("../migrations/0001_dovecote.sql"),
119    MigrationCompatibility::new(CrateVersion::new(0, 1, 0), None),
120    false,
121)];
122
123pub(crate) fn current_crate_version() -> CrateVersion {
124    CrateVersion::new(
125        env!("CARGO_PKG_VERSION_MAJOR")
126            .parse()
127            .expect("Cargo version is numeric"),
128        env!("CARGO_PKG_VERSION_MINOR")
129            .parse()
130            .expect("Cargo version is numeric"),
131        env!("CARGO_PKG_VERSION_PATCH")
132            .parse()
133            .expect("Cargo version is numeric"),
134    )
135}
136pub(crate) fn current_migration() -> Result<Migration, String> {
137    MIGRATIONS
138        .iter()
139        .find(|migration| migration.version() == SCHEMA_VERSION)
140        .copied()
141        .ok_or_else(|| format!("adapter does not ship schema version {SCHEMA_VERSION}"))
142}
143pub(crate) fn migration_is_usable(migration: Migration) -> Result<(), String> {
144    if !migration.compatibility().contains(current_crate_version()) {
145        return Err("current crate is outside migration compatibility range".to_owned());
146    }
147    Ok(())
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    #[test]
154    fn migrations_are_ordered_and_typed() {
155        assert_eq!(MIGRATIONS[0].version(), SCHEMA_VERSION);
156        assert!(!MIGRATIONS[0].sql().is_empty());
157        assert_eq!(
158            MIGRATIONS[0].compatibility().minimum(),
159            CrateVersion::new(0, 1, 0)
160        );
161        assert!(!MIGRATIONS[0].sql().contains("dovecote_schema"));
162        assert!(!MIGRATIONS[0].rolling_compatible());
163        assert!(
164            MIGRATIONS[0]
165                .compatibility()
166                .contains(CrateVersion::new(0, 9, 0))
167        );
168        assert!(
169            MigrationCompatibility::try_new(
170                CrateVersion::new(1, 0, 0),
171                Some(CrateVersion::new(0, 9, 0))
172            )
173            .is_err()
174        );
175    }
176}