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 #[must_use]
20 pub const fn new(major: u16, minor: u16, patch: u16) -> Self {
21 Self {
22 major,
23 minor,
24 patch,
25 }
26 }
27
28 #[must_use]
30 pub const fn major(self) -> u16 {
31 self.major
32 }
33
34 #[must_use]
36 pub const fn minor(self) -> u16 {
37 self.minor
38 }
39
40 #[must_use]
42 pub const fn patch(self) -> u16 {
43 self.patch
44 }
45
46 const fn is_less_than(self, other: Self) -> bool {
47 self.major < other.major
48 || (self.major == other.major
49 && (self.minor < other.minor
50 || (self.minor == other.minor && self.patch < other.patch)))
51 }
52}
53
54pub(crate) fn current_crate_version() -> CrateVersion {
55 CrateVersion::new(
56 env!("CARGO_PKG_VERSION_MAJOR")
57 .parse()
58 .expect("Cargo supplies a numeric major version"),
59 env!("CARGO_PKG_VERSION_MINOR")
60 .parse()
61 .expect("Cargo supplies a numeric minor version"),
62 env!("CARGO_PKG_VERSION_PATCH")
63 .parse()
64 .expect("Cargo supplies a numeric patch version"),
65 )
66}
67
68#[derive(Debug, FromRow)]
69pub(crate) struct SchemaMarker {
70 pub(crate) schema_version: i32,
71 pub(crate) minimum_crate_major: i16,
72 pub(crate) minimum_crate_minor: i16,
73 pub(crate) minimum_crate_patch: i16,
74 pub(crate) rolling_compatible: bool,
75}
76
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub struct MigrationCompatibilityError;
80
81impl std::fmt::Display for MigrationCompatibilityError {
82 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 formatter.write_str("migration compatibility maximum precedes minimum")
84 }
85}
86
87impl std::error::Error for MigrationCompatibilityError {}
88
89#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub struct MigrationCompatibility {
94 minimum: CrateVersion,
95 maximum: Option<CrateVersion>,
96}
97
98impl MigrationCompatibility {
99 const fn new(minimum: CrateVersion, maximum: Option<CrateVersion>) -> Self {
100 Self { minimum, maximum }
101 }
102
103 pub const fn try_new(
108 minimum: CrateVersion,
109 maximum: Option<CrateVersion>,
110 ) -> Result<Self, MigrationCompatibilityError> {
111 if let Some(maximum) = maximum
112 && maximum.is_less_than(minimum)
113 {
114 return Err(MigrationCompatibilityError);
115 }
116 Ok(Self::new(minimum, maximum))
117 }
118
119 #[must_use]
121 pub const fn minimum(self) -> CrateVersion {
122 self.minimum
123 }
124
125 #[must_use]
127 pub const fn maximum(self) -> Option<CrateVersion> {
128 self.maximum
129 }
130
131 #[must_use]
133 pub const fn contains(self, version: CrateVersion) -> bool {
134 !version.is_less_than(self.minimum)
135 && match self.maximum {
136 Some(maximum) => !maximum.is_less_than(version),
137 None => true,
138 }
139 }
140}
141
142#[derive(Clone, Copy, Debug, Eq, PartialEq)]
145pub struct Migration {
146 version: u32,
147 sql: &'static str,
148 compatibility: MigrationCompatibility,
149 rolling_compatible: bool,
150}
151
152impl Migration {
153 const fn new(
154 version: u32,
155 sql: &'static str,
156 compatibility: MigrationCompatibility,
157 rolling_compatible: bool,
158 ) -> Self {
159 Self {
160 version,
161 sql,
162 compatibility,
163 rolling_compatible,
164 }
165 }
166
167 #[must_use]
169 pub const fn version(self) -> u32 {
170 self.version
171 }
172
173 #[must_use]
175 pub const fn sql(self) -> &'static str {
176 self.sql
177 }
178
179 #[must_use]
181 pub const fn compatibility(self) -> MigrationCompatibility {
182 self.compatibility
183 }
184
185 #[must_use]
187 pub const fn rolling_compatible(self) -> bool {
188 self.rolling_compatible
189 }
190}
191
192pub const MIGRATIONS: &[Migration] = &[Migration::new(
198 2,
199 include_str!("../migrations/0002_dovecote_tenant_baseline.sql"),
200 MigrationCompatibility::new(CrateVersion::new(0, 2, 0), None),
201 false,
202)];
203
204pub const LEGACY_MIGRATION: Migration = Migration::new(
206 1,
207 include_str!("../migrations/0001_dovecote.sql"),
208 MigrationCompatibility::new(CrateVersion::new(0, 1, 0), None),
209 false,
210);
211
212pub const V1_TENANT_PREPARE_SQL: &str =
214 include_str!("../migrations/0002_dovecote_tenant_prepare.sql");
215
216#[deprecated(
218 since = "0.2.1",
219 note = "the published 0.2.0 artifact cannot complete; use V1_TENANT_ACTIVATE_V2_SQL"
220)]
221pub const V1_TENANT_ACTIVATE_SQL: &str =
222 include_str!("../migrations/0002_dovecote_tenant_activate.sql");
223
224pub const V1_TENANT_ACTIVATE_V2_SQL: &str =
231 include_str!("../migrations/0002_dovecote_tenant_activate_v2.sql");
232
233pub(crate) fn current_migration() -> Result<Migration, String> {
234 MIGRATIONS
235 .iter()
236 .find(|migration| migration.version() == SCHEMA_VERSION)
237 .copied()
238 .ok_or_else(|| format!("adapter does not ship schema version {SCHEMA_VERSION}"))
239}
240
241pub(crate) fn marker_compatibility(
242 marker: &SchemaMarker,
243) -> Result<MigrationCompatibility, String> {
244 let minimum = CrateVersion::new(
245 u16::try_from(marker.minimum_crate_major)
246 .map_err(|_| "schema marker minimum major version is negative".to_owned())?,
247 u16::try_from(marker.minimum_crate_minor)
248 .map_err(|_| "schema marker minimum minor version is negative".to_owned())?,
249 u16::try_from(marker.minimum_crate_patch)
250 .map_err(|_| "schema marker minimum patch version is negative".to_owned())?,
251 );
252 MigrationCompatibility::try_new(minimum, None)
253 .map_err(|error| format!("schema marker compatibility is invalid: {error}"))
254}
255
256pub(crate) fn marker_matches_migration(
257 marker: &SchemaMarker,
258 migration: Migration,
259) -> Result<(), String> {
260 let marker_version = u32::try_from(marker.schema_version)
261 .map_err(|_| "schema marker version is negative".to_owned())?;
262 let compatibility = marker_compatibility(marker)?;
263 if marker_version != migration.version() {
264 return Err(format!(
265 "schema marker version {} does not match installed version {}",
266 marker_version,
267 migration.version()
268 ));
269 }
270
271 if compatibility != migration.compatibility() {
272 return Err("schema marker compatibility range is incompatible".to_owned());
273 }
274
275 if marker.rolling_compatible != migration.rolling_compatible() {
276 return Err("schema marker rolling compatibility is incompatible".to_owned());
277 }
278
279 if !migration.compatibility().contains(current_crate_version()) {
280 return Err("current crate is outside the migration compatibility range".to_owned());
281 }
282 Ok(())
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn migrations_are_ordered_and_typed() {
291 assert_eq!(MIGRATIONS[0].version(), SCHEMA_VERSION);
292 assert!(!MIGRATIONS[0].sql().is_empty());
293 assert_eq!(
294 MIGRATIONS[0].compatibility().minimum(),
295 CrateVersion::new(0, 2, 0)
296 );
297 assert!(!MIGRATIONS[0].rolling_compatible());
298 assert_eq!(LEGACY_MIGRATION.version(), 1);
299 assert!(!LEGACY_MIGRATION.sql().is_empty());
300 assert!(
301 MIGRATIONS[0]
302 .compatibility()
303 .contains(CrateVersion::new(0, 9, 0))
304 );
305 assert!(
306 MigrationCompatibility::try_new(
307 CrateVersion::new(1, 0, 0),
308 Some(CrateVersion::new(0, 9, 0))
309 )
310 .is_err()
311 );
312 }
313}