1use crate::conformance::{Defect, Defective, Failure};
27use crate::{
28 Dialect, MigrateError, MigrateReport, Migrates, Migration, RelError, RelationalBackend, Value,
29};
30use persistant_core::{CapabilityId, Config, Controls, Reason, Row as MatrixRow, Verdict};
31use std::collections::BTreeMap;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[non_exhaustive]
36pub enum MigrateLeaf {
37 Apply,
39 Checksum,
41 Newer,
43 Atomic,
45 DdlAtomic,
47}
48
49impl MigrateLeaf {
50 pub const ALL: [MigrateLeaf; 5] = [
52 MigrateLeaf::Apply,
53 MigrateLeaf::Checksum,
54 MigrateLeaf::Newer,
55 MigrateLeaf::Atomic,
56 MigrateLeaf::DdlAtomic,
57 ];
58
59 pub fn id(self) -> &'static str {
61 match self {
62 MigrateLeaf::Apply => "rel.migrate.apply",
63 MigrateLeaf::Checksum => "rel.migrate.checksum",
64 MigrateLeaf::Newer => "rel.migrate.newer",
65 MigrateLeaf::Atomic => "rel.migrate.atomic",
66 MigrateLeaf::DdlAtomic => "rel.migrate.ddl_atomic",
67 }
68 }
69}
70
71const T: &str = "persistant_mig_t";
72const T2: &str = "persistant_mig_t2";
73
74fn wrong<X>(msg: impl Into<String>) -> Result<X, Failure> {
75 Err(Failure::Wrong(msg.into()))
76}
77
78fn mig(version: i64, sql: &str) -> Migration {
79 Migration::new(version, format!("persistant migrate leaf {version}"), sql)
80}
81
82fn create_t() -> Migration {
83 mig(
84 1,
85 &format!("CREATE TABLE {T} (id BIGINT PRIMARY KEY, n BIGINT NOT NULL)"),
86 )
87}
88
89async fn reset<M: Migrates>(m: &M) -> Result<(), RelError> {
90 for t in [T, T2, "_sqlx_migrations"] {
91 m.execute(&format!("DROP TABLE IF EXISTS {t}"), &[]).await?;
92 }
93 Ok(())
94}
95
96async fn count<M: Migrates>(m: &M, table: &str) -> Result<i64, RelError> {
97 let rows = m
98 .query(&format!("SELECT COUNT(*) FROM {table}"), &[])
99 .await?;
100 Ok(rows.first().and_then(|r| r.int(0)).unwrap_or(-1))
101}
102
103fn migrated<X>(r: Result<X, MigrateError>, what: &str) -> Result<X, Failure> {
104 r.or_else(|e| wrong(format!("{what}: {e}")))
105}
106
107pub async fn run_leaf<M: Migrates>(leaf: MigrateLeaf, m: &M) -> Result<(), Failure> {
110 if matches!(leaf, MigrateLeaf::DdlAtomic)
111 && matches!(m.dialect(), Dialect::MySql | Dialect::MariaDb)
112 {
113 return Err(Failure::Backend(RelError::Unsupported(
114 "DDL commits implicitly: a failed migration cannot undo the tables it created"
115 .to_string(),
116 )));
117 }
118 reset(m).await?;
119 let result = match leaf {
120 MigrateLeaf::Apply => apply(m).await,
121 MigrateLeaf::Checksum => checksum(m).await,
122 MigrateLeaf::Newer => newer(m).await,
123 MigrateLeaf::Atomic => atomic(m).await,
124 MigrateLeaf::DdlAtomic => ddl_atomic(m).await,
125 };
126 reset(m).await?;
127 result
128}
129
130async fn apply<M: Migrates>(m: &M) -> Result<(), Failure> {
131 let migs = [
132 create_t(),
133 mig(2, &format!("INSERT INTO {T} (id, n) VALUES (1, 1)")),
134 ];
135 let first = migrated(m.migrate(&migs).await, "the first run")?;
136 if first.applied != [1, 2] || !first.already_applied.is_empty() {
137 return wrong(format!(
138 "the first run reported {first:?}, not [1, 2] applied"
139 ));
140 }
141 if count(m, T).await? != 1 {
142 return wrong("migration 2 did not leave its row");
143 }
144 let ledger = migrated(m.applied_versions().await, "reading the ledger")?;
145 if ledger != [1, 2] {
146 return wrong(format!("the ledger lists {ledger:?}, not [1, 2]"));
147 }
148 let again = migrated(m.migrate(&migs).await, "the second run")?;
149 let want = MigrateReport {
150 applied: vec![],
151 already_applied: vec![1, 2],
152 };
153 if again != want {
154 return wrong(format!("the second run reported {again:?}, not {want:?}"));
155 }
156 if count(m, T).await? != 1 {
157 return wrong("the second run ran a migration again");
158 }
159 Ok(())
160}
161
162async fn checksum<M: Migrates>(m: &M) -> Result<(), Failure> {
163 let migs = [
164 create_t(),
165 mig(2, &format!("INSERT INTO {T} (id, n) VALUES (1, 1)")),
166 ];
167 migrated(m.migrate(&migs).await, "the first run")?;
168 let edited = [
169 create_t(),
170 mig(2, &format!("INSERT INTO {T} (id, n) VALUES (1, 2)")),
171 ];
172 match m.migrate(&edited).await {
173 Err(MigrateError::ChecksumMismatch { version: 2 }) => Ok(()),
174 other => wrong(format!(
175 "an applied migration was edited and the run answered {other:?}, not a checksum mismatch on 2"
176 )),
177 }
178}
179
180async fn newer<M: Migrates>(m: &M) -> Result<(), Failure> {
181 let migs = [
182 create_t(),
183 mig(2, &format!("INSERT INTO {T} (id, n) VALUES (1, 1)")),
184 ];
185 migrated(m.migrate(&migs).await, "the first run")?;
186 match m.migrate(&migs[..1]).await {
187 Err(MigrateError::NewerThanProgram { version: 2 }) => Ok(()),
188 other => wrong(format!(
189 "a program that knows only migration 1 met a database at 2 and the run answered {other:?}"
190 )),
191 }
192}
193
194async fn atomic<M: Migrates>(m: &M) -> Result<(), Failure> {
195 let migs = [
196 create_t(),
197 mig(
198 2,
199 &format!(
200 "INSERT INTO {T} (id, n) VALUES (1, 1); INSERT INTO {T} (id, n) VALUES (1, 2)"
201 ),
202 ),
203 ];
204 match m.migrate(&migs).await {
205 Err(MigrateError::Failed {
206 version: 2,
207 error: RelError::UniqueViolation { .. },
208 }) => {}
209 other => {
210 return wrong(format!(
211 "a migration whose second statement repeats a key answered {other:?}, not a classified failure of 2"
212 ));
213 }
214 }
215 if count(m, T).await? != 0 {
216 return wrong("the failed migration left the rows of its first statement behind");
217 }
218 let ledger = migrated(m.applied_versions().await, "reading the ledger")?;
219 if ledger != [1] {
220 return wrong(format!(
221 "after the failure the ledger lists {ledger:?}, not [1]"
222 ));
223 }
224 Ok(())
225}
226
227async fn ddl_atomic<M: Migrates>(m: &M) -> Result<(), Failure> {
228 let migs = [
229 create_t(),
230 mig(
231 2,
232 &format!(
233 "CREATE TABLE {T2} (id BIGINT PRIMARY KEY); INSERT INTO {T} (id, n) VALUES (1, 1); INSERT INTO {T} (id, n) VALUES (1, 2)"
234 ),
235 ),
236 ];
237 if !matches!(
238 m.migrate(&migs).await,
239 Err(MigrateError::Failed { version: 2, .. })
240 ) {
241 return wrong("the failing migration was not reported as failed");
242 }
243 count(m, T).await?;
245 if count(m, T2).await.is_ok() {
246 return wrong("the failed migration left the table it created behind");
247 }
248 Ok(())
249}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct MigrationCertified {
254 pub leaf: MigrateLeaf,
256 pub row: MatrixRow,
258}
259
260pub async fn certify<M: Migrates>(
263 target: &M,
264 config: &Config,
265 metadata: &Config,
266) -> Vec<MigrationCertified> {
267 let mut out = Vec::new();
268 for leaf in MigrateLeaf::ALL {
269 let result = run_leaf(leaf, target).await;
270 let positive_passed = !matches!(
271 &result,
272 Err(Failure::Backend(
273 RelError::Unsupported(_) | RelError::Unavailable(_)
274 ))
275 );
276 let bad = Defective::new(target, Defect::NoMigrationLedger);
277 let known_bad_failed = run_leaf(leaf, &bad).await.is_err();
278 let controls = Controls {
279 positive_passed,
280 known_bad_failed,
281 };
282 let id = CapabilityId::new(leaf.id()).expect("leaf id is valid");
283 let md = metadata.clone().with("test", format!("rel::{}", leaf.id()));
284 let off = |reason: Reason, note: String| MatrixRow {
285 subject_kind: "capability".to_string(),
286 subject: id.clone(),
287 backend: target.backend().to_string(),
288 version: target.version().to_string(),
289 config: config.clone(),
290 enabled: false,
291 reason: Some(reason),
292 metadata: md.clone(),
293 note: note.replace(['\t', '\n'], " "),
294 extra: BTreeMap::new(),
295 };
296 let row = match (
297 &result,
298 controls.positive_passed && controls.known_bad_failed,
299 ) {
300 (Ok(()), true) => Verdict::Exact
301 .into_row(
302 id.clone(),
303 target.backend(),
304 target.version(),
305 config.clone(),
306 md.clone(),
307 controls,
308 )
309 .expect("both controls hold"),
310 (Ok(()), false) => off(
311 Reason::Untested,
312 format!("controls did not hold: {controls:?}"),
313 ),
314 (Err(Failure::Wrong(m)), _) => off(Reason::Wrong, m.clone()),
315 (Err(Failure::Backend(e)), _) => off(Reason::Missing, e.to_string()),
316 };
317 out.push(MigrationCertified { leaf, row });
318 }
319 out
320}
321
322impl<R: RelationalBackend> Migrates for Defective<'_, R> {
325 async fn migrate(&self, migrations: &[Migration]) -> Result<MigrateReport, MigrateError> {
326 if self.defect() != Defect::NoMigrationLedger {
327 return Err(MigrateError::Backend(
328 "this defect does not wrap a migrator".to_string(),
329 ));
330 }
331 let mut applied = Vec::new();
332 for m in migrations {
333 for stmt in m.sql.split(';').map(str::trim).filter(|s| !s.is_empty()) {
334 self.inner()
335 .execute(stmt, &[] as &[Value])
336 .await
337 .map_err(|error| MigrateError::Failed {
338 version: m.version,
339 error,
340 })?;
341 }
342 applied.push(m.version);
343 }
344 Ok(MigrateReport {
345 applied,
346 already_applied: Vec::new(),
347 })
348 }
349
350 async fn applied_versions(&self) -> Result<Vec<i64>, MigrateError> {
351 Ok(Vec::new())
352 }
353}