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