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
202#[deprecated(
204 since = "0.2.1",
205 note = "the published 0.2.0 artifact cannot complete; use V1_TENANT_ACTIVATE_V2_SQL"
206)]
207pub const V1_TENANT_ACTIVATE_SQL: &str =
208 include_str!("../migrations/0002_dovecote_tenant_activate.sql");
209
210pub const V1_TENANT_ACTIVATE_V2_SQL: &str =
217 include_str!("../migrations/0002_dovecote_tenant_activate_v2.sql");
218
219pub(crate) fn current_migration() -> Result<Migration, String> {
220 MIGRATIONS
221 .iter()
222 .find(|migration| migration.version() == SCHEMA_VERSION)
223 .copied()
224 .ok_or_else(|| format!("adapter does not ship schema version {SCHEMA_VERSION}"))
225}
226
227pub(crate) fn marker_compatibility(
228 marker: &SchemaMarker,
229) -> Result<MigrationCompatibility, String> {
230 let minimum = CrateVersion::new(
231 u16::try_from(marker.minimum_crate_major)
232 .map_err(|_| "schema marker minimum major version is negative".to_owned())?,
233 u16::try_from(marker.minimum_crate_minor)
234 .map_err(|_| "schema marker minimum minor version is negative".to_owned())?,
235 u16::try_from(marker.minimum_crate_patch)
236 .map_err(|_| "schema marker minimum patch version is negative".to_owned())?,
237 );
238 MigrationCompatibility::try_new(minimum, None)
239 .map_err(|error| format!("schema marker compatibility is invalid: {error}"))
240}
241
242pub(crate) fn marker_matches_migration(
243 marker: &SchemaMarker,
244 migration: Migration,
245) -> Result<(), String> {
246 let marker_version = u32::try_from(marker.schema_version)
247 .map_err(|_| "schema marker version is negative".to_owned())?;
248 let compatibility = marker_compatibility(marker)?;
249 if marker_version != migration.version() {
250 return Err(format!(
251 "schema marker version {} does not match installed version {}",
252 marker_version,
253 migration.version()
254 ));
255 }
256
257 if compatibility != migration.compatibility() {
258 return Err("schema marker compatibility range is incompatible".to_owned());
259 }
260
261 if marker.rolling_compatible != migration.rolling_compatible() {
262 return Err("schema marker rolling compatibility is incompatible".to_owned());
263 }
264
265 if !migration.compatibility().contains(current_crate_version()) {
266 return Err("current crate is outside the migration compatibility range".to_owned());
267 }
268 Ok(())
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn migrations_are_ordered_and_typed() {
277 assert_eq!(MIGRATIONS[0].version(), SCHEMA_VERSION);
278 assert!(!MIGRATIONS[0].sql().is_empty());
279 assert_eq!(
280 MIGRATIONS[0].compatibility().minimum(),
281 CrateVersion::new(0, 2, 0)
282 );
283 assert!(!MIGRATIONS[0].rolling_compatible());
284 assert_eq!(LEGACY_MIGRATION.version(), 1);
285 assert!(!LEGACY_MIGRATION.sql().is_empty());
286 assert!(
287 MIGRATIONS[0]
288 .compatibility()
289 .contains(CrateVersion::new(0, 9, 0))
290 );
291 assert!(
292 MigrationCompatibility::try_new(
293 CrateVersion::new(1, 0, 0),
294 Some(CrateVersion::new(0, 9, 0))
295 )
296 .is_err()
297 );
298 }
299}