1use sqlx::FromRow;
4
5pub const SCHEMA_VERSION: u32 = 1;
7
8#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
11pub struct CrateVersion {
12 major: u16,
13 minor: u16,
14 patch: u16,
15}
16
17impl CrateVersion {
18 pub const fn new(major: u16, minor: u16, patch: u16) -> Self {
19 Self {
20 major,
21 minor,
22 patch,
23 }
24 }
25
26 pub const fn major(self) -> u16 {
27 self.major
28 }
29
30 pub const fn minor(self) -> u16 {
31 self.minor
32 }
33
34 pub const fn patch(self) -> u16 {
35 self.patch
36 }
37
38 const fn is_less_than(self, other: Self) -> bool {
39 self.major < other.major
40 || (self.major == other.major
41 && (self.minor < other.minor
42 || (self.minor == other.minor && self.patch < other.patch)))
43 }
44}
45
46pub(crate) fn current_crate_version() -> CrateVersion {
47 CrateVersion::new(
48 env!("CARGO_PKG_VERSION_MAJOR")
49 .parse()
50 .expect("Cargo supplies a numeric major version"),
51 env!("CARGO_PKG_VERSION_MINOR")
52 .parse()
53 .expect("Cargo supplies a numeric minor version"),
54 env!("CARGO_PKG_VERSION_PATCH")
55 .parse()
56 .expect("Cargo supplies a numeric patch version"),
57 )
58}
59
60#[derive(Debug, FromRow)]
61pub(crate) struct SchemaMarker {
62 pub(crate) schema_version: i32,
63 pub(crate) minimum_crate_major: i16,
64 pub(crate) minimum_crate_minor: i16,
65 pub(crate) minimum_crate_patch: i16,
66 pub(crate) rolling_compatible: bool,
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub struct MigrationCompatibilityError;
72
73impl std::fmt::Display for MigrationCompatibilityError {
74 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 formatter.write_str("migration compatibility maximum precedes minimum")
76 }
77}
78
79impl std::error::Error for MigrationCompatibilityError {}
80
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85pub struct MigrationCompatibility {
86 minimum: CrateVersion,
87 maximum: Option<CrateVersion>,
88}
89
90impl MigrationCompatibility {
91 const fn new(minimum: CrateVersion, maximum: Option<CrateVersion>) -> Self {
92 Self { minimum, maximum }
93 }
94
95 pub const fn try_new(
96 minimum: CrateVersion,
97 maximum: Option<CrateVersion>,
98 ) -> Result<Self, MigrationCompatibilityError> {
99 if let Some(maximum) = maximum
100 && maximum.is_less_than(minimum)
101 {
102 return Err(MigrationCompatibilityError);
103 }
104 Ok(Self::new(minimum, maximum))
105 }
106
107 pub const fn minimum(self) -> CrateVersion {
108 self.minimum
109 }
110
111 pub const fn maximum(self) -> Option<CrateVersion> {
112 self.maximum
113 }
114
115 pub const fn contains(self, version: CrateVersion) -> bool {
116 !version.is_less_than(self.minimum)
117 && match self.maximum {
118 Some(maximum) => !maximum.is_less_than(version),
119 None => true,
120 }
121 }
122}
123
124#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub struct Migration {
128 version: u32,
129 sql: &'static str,
130 compatibility: MigrationCompatibility,
131 rolling_compatible: bool,
132}
133
134impl Migration {
135 const fn new(
136 version: u32,
137 sql: &'static str,
138 compatibility: MigrationCompatibility,
139 rolling_compatible: bool,
140 ) -> Self {
141 Self {
142 version,
143 sql,
144 compatibility,
145 rolling_compatible,
146 }
147 }
148
149 pub const fn version(self) -> u32 {
150 self.version
151 }
152
153 pub const fn sql(self) -> &'static str {
154 self.sql
155 }
156
157 pub const fn compatibility(self) -> MigrationCompatibility {
158 self.compatibility
159 }
160
161 pub const fn rolling_compatible(self) -> bool {
162 self.rolling_compatible
163 }
164}
165
166pub const MIGRATIONS: &[Migration] = &[Migration::new(
168 1,
169 include_str!("../migrations/0001_dovecote.sql"),
170 MigrationCompatibility::new(CrateVersion::new(0, 1, 0), None),
171 false,
172)];
173
174pub(crate) fn current_migration() -> Result<Migration, String> {
175 MIGRATIONS
176 .iter()
177 .find(|migration| migration.version() == SCHEMA_VERSION)
178 .copied()
179 .ok_or_else(|| format!("adapter does not ship schema version {SCHEMA_VERSION}"))
180}
181
182pub(crate) fn marker_compatibility(
183 marker: &SchemaMarker,
184) -> Result<MigrationCompatibility, String> {
185 let minimum = CrateVersion::new(
186 u16::try_from(marker.minimum_crate_major)
187 .map_err(|_| "schema marker minimum major version is negative".to_owned())?,
188 u16::try_from(marker.minimum_crate_minor)
189 .map_err(|_| "schema marker minimum minor version is negative".to_owned())?,
190 u16::try_from(marker.minimum_crate_patch)
191 .map_err(|_| "schema marker minimum patch version is negative".to_owned())?,
192 );
193 MigrationCompatibility::try_new(minimum, None)
194 .map_err(|error| format!("schema marker compatibility is invalid: {error}"))
195}
196
197pub(crate) fn marker_matches_migration(
198 marker: &SchemaMarker,
199 migration: Migration,
200) -> Result<(), String> {
201 let marker_version = u32::try_from(marker.schema_version)
202 .map_err(|_| "schema marker version is negative".to_owned())?;
203 let compatibility = marker_compatibility(marker)?;
204 if marker_version != migration.version() {
205 return Err(format!(
206 "schema marker version {} does not match installed version {}",
207 marker_version,
208 migration.version()
209 ));
210 }
211
212 if compatibility != migration.compatibility() {
213 return Err("schema marker compatibility range is incompatible".to_owned());
214 }
215
216 if marker.rolling_compatible != migration.rolling_compatible() {
217 return Err("schema marker rolling compatibility is incompatible".to_owned());
218 }
219
220 if !migration.compatibility().contains(current_crate_version()) {
221 return Err("current crate is outside the migration compatibility range".to_owned());
222 }
223 Ok(())
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn migrations_are_ordered_and_typed() {
232 assert_eq!(MIGRATIONS[0].version(), SCHEMA_VERSION);
233 assert!(!MIGRATIONS[0].sql().is_empty());
234 assert_eq!(
235 MIGRATIONS[0].compatibility().minimum(),
236 CrateVersion::new(0, 1, 0)
237 );
238 assert!(!MIGRATIONS[0].rolling_compatible());
239 assert!(
240 MIGRATIONS[0]
241 .compatibility()
242 .contains(CrateVersion::new(0, 9, 0))
243 );
244 assert!(
245 MigrationCompatibility::try_new(
246 CrateVersion::new(1, 0, 0),
247 Some(CrateVersion::new(0, 9, 0))
248 )
249 .is_err()
250 );
251 }
252}