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