1use crate::{Dialect, Isolation, RelError, RelationalBackend, RelationalTx, Row, Value};
42use persistant_core::{CapabilityId, Config, Controls, Reason, Row as MatrixRow, Verdict};
43use std::collections::BTreeMap;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[non_exhaustive]
48pub enum Leaf {
49 Values,
51 Transaction,
53 UniqueClassified,
55 ForeignKey,
57 CheckConstraint,
59 IsolationNoDirtyRead,
61 IsolationRepeatableRead,
63 IsolationSerializable,
65 Savepoint,
67 RowLock,
69 UpsertWhere,
71}
72
73impl Leaf {
74 pub const ALL: [Leaf; 11] = [
76 Leaf::Values,
77 Leaf::Transaction,
78 Leaf::UniqueClassified,
79 Leaf::ForeignKey,
80 Leaf::CheckConstraint,
81 Leaf::IsolationNoDirtyRead,
82 Leaf::IsolationRepeatableRead,
83 Leaf::IsolationSerializable,
84 Leaf::Savepoint,
85 Leaf::RowLock,
86 Leaf::UpsertWhere,
87 ];
88
89 pub fn id(self) -> &'static str {
91 match self {
92 Leaf::Values => "rel.values",
93 Leaf::Transaction => "rel.transaction",
94 Leaf::UniqueClassified => "rel.error.unique",
95 Leaf::ForeignKey => "rel.foreign_key",
96 Leaf::CheckConstraint => "rel.check_constraint",
97 Leaf::IsolationNoDirtyRead => "rel.isolation.no_dirty_read",
98 Leaf::IsolationRepeatableRead => "rel.isolation.repeatable_read",
99 Leaf::IsolationSerializable => "rel.isolation.serializable",
100 Leaf::Savepoint => "rel.savepoint",
101 Leaf::RowLock => "rel.row_lock",
102 Leaf::UpsertWhere => "rel.upsert_where",
103 }
104 }
105
106 fn defects(self) -> &'static [Defect] {
107 match self {
108 Leaf::Values => &[Defect::LossyBytes],
109 Leaf::Transaction
110 | Leaf::IsolationNoDirtyRead
111 | Leaf::IsolationRepeatableRead
112 | Leaf::IsolationSerializable
113 | Leaf::Savepoint
114 | Leaf::RowLock => &[Defect::NoTransactions],
115 Leaf::UniqueClassified => &[Defect::UnclassifiedErrors],
116 Leaf::ForeignKey | Leaf::CheckConstraint | Leaf::UpsertWhere => &[],
117 }
118 }
119
120 fn has_unconstrained_control(self) -> bool {
122 matches!(
123 self,
124 Leaf::ForeignKey | Leaf::CheckConstraint | Leaf::UpsertWhere
125 )
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131#[non_exhaustive]
132pub enum Defect {
133 NoTransactions,
136 LossyBytes,
138 UnclassifiedErrors,
140 NoMigrationLedger,
143}
144
145#[derive(Debug)]
147pub struct Defective<'a, R> {
148 inner: &'a R,
149 defect: Defect,
150}
151
152impl<'a, R: RelationalBackend> Defective<'a, R> {
153 pub fn new(inner: &'a R, defect: Defect) -> Self {
155 Defective { inner, defect }
156 }
157
158 pub(crate) fn inner(&self) -> &'a R {
159 self.inner
160 }
161
162 pub(crate) fn defect(&self) -> Defect {
163 self.defect
164 }
165}
166
167pub enum DefectiveTx<'a, R: RelationalBackend + 'a> {
169 Real(R::Tx<'a>, Defect),
171 Direct(&'a R),
173}
174
175impl<R: RelationalBackend> RelationalBackend for Defective<'_, R> {
176 type Tx<'a>
177 = DefectiveTx<'a, R>
178 where
179 Self: 'a;
180
181 fn backend(&self) -> &str {
182 self.inner.backend()
183 }
184
185 fn version(&self) -> &str {
186 self.inner.version()
187 }
188
189 fn dialect(&self) -> Dialect {
190 self.inner.dialect()
191 }
192
193 async fn execute(&self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
194 self.inner
195 .execute(sql, &lossy(self.defect, params))
196 .await
197 .map_err(|e| unclassify(self.defect, e))
198 }
199
200 async fn query(&self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
201 self.inner
202 .query(sql, &lossy(self.defect, params))
203 .await
204 .map_err(|e| unclassify(self.defect, e))
205 }
206
207 async fn begin(&self, isolation: Isolation) -> Result<DefectiveTx<'_, R>, RelError> {
208 if self.defect == Defect::NoTransactions {
209 return Ok(DefectiveTx::Direct(self.inner));
210 }
211 Ok(DefectiveTx::Real(
212 self.inner
213 .begin(isolation)
214 .await
215 .map_err(|e| unclassify(self.defect, e))?,
216 self.defect,
217 ))
218 }
219}
220
221fn lossy(defect: Defect, params: &[Value]) -> Vec<Value> {
222 params
223 .iter()
224 .map(|p| match (defect, p) {
225 (Defect::LossyBytes, Value::Bytes(b)) => {
226 Value::Bytes(b.iter().take(1).copied().collect())
227 }
228 _ => p.clone(),
229 })
230 .collect()
231}
232
233fn unclassify(defect: Defect, e: RelError) -> RelError {
234 if defect == Defect::UnclassifiedErrors {
235 RelError::Backend(e.to_string())
236 } else {
237 e
238 }
239}
240
241impl<R: RelationalBackend> RelationalTx for DefectiveTx<'_, R> {
242 async fn execute(&mut self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
243 match self {
244 DefectiveTx::Real(tx, d) => tx
245 .execute(sql, &lossy(*d, params))
246 .await
247 .map_err(|e| unclassify(*d, e)),
248 DefectiveTx::Direct(_)
251 if sql.starts_with("SAVEPOINT")
252 || sql.starts_with("RELEASE")
253 || sql.starts_with("ROLLBACK TO") =>
254 {
255 Ok(0)
256 }
257 DefectiveTx::Direct(db) => db.execute(sql, params).await,
258 }
259 }
260
261 async fn query(&mut self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
262 match self {
263 DefectiveTx::Real(tx, d) => tx
264 .query(sql, &lossy(*d, params))
265 .await
266 .map_err(|e| unclassify(*d, e)),
267 DefectiveTx::Direct(db) => db.query(sql, params).await,
268 }
269 }
270
271 async fn commit(self) -> Result<(), RelError> {
272 match self {
273 DefectiveTx::Real(tx, d) => tx.commit().await.map_err(|e| unclassify(d, e)),
274 DefectiveTx::Direct(_) => Ok(()),
275 }
276 }
277
278 async fn rollback(self) -> Result<(), RelError> {
279 match self {
280 DefectiveTx::Real(tx, d) => tx.rollback().await.map_err(|e| unclassify(d, e)),
281 DefectiveTx::Direct(_) => Ok(()),
282 }
283 }
284}
285
286#[derive(Debug, Clone, PartialEq, Eq)]
288pub enum Failure {
289 Wrong(String),
291 Backend(RelError),
293}
294
295impl From<RelError> for Failure {
296 fn from(e: RelError) -> Self {
297 Failure::Backend(e)
298 }
299}
300
301fn wrong<T>(msg: impl Into<String>) -> Result<T, Failure> {
302 Err(Failure::Wrong(msg.into()))
303}
304
305#[derive(Clone, Copy)]
308pub(crate) enum Ty {
309 Int,
310 Text,
311 Bytes,
312 Float,
313}
314
315pub(crate) struct Sql(pub(crate) Dialect);
316
317impl Sql {
318 pub(crate) fn p(&self, n: usize) -> String {
320 match self.0 {
321 Dialect::Postgres => format!("${n}"),
322 Dialect::Sqlite => format!("?{n}"),
323 _ => "?".to_string(),
324 }
325 }
326
327 pub(crate) fn ty(&self, t: Ty) -> &'static str {
328 match (&self.0, t) {
329 (_, Ty::Int) => "BIGINT",
330 (Dialect::Postgres, Ty::Text) => "TEXT",
331 (Dialect::Sqlite, Ty::Text) => "TEXT",
332 (_, Ty::Text) => "VARCHAR(255)",
333 (Dialect::Postgres, Ty::Bytes) => "BYTEA",
334 (Dialect::Sqlite, Ty::Bytes) => "BLOB",
335 (_, Ty::Bytes) => "LONGBLOB",
336 (Dialect::Postgres, Ty::Float) => "DOUBLE PRECISION",
337 (_, Ty::Float) => "DOUBLE",
338 }
339 }
340
341 pub(crate) fn tail(&self) -> &'static str {
343 match self.0 {
344 Dialect::MySql | Dialect::MariaDb => " ENGINE=InnoDB",
345 _ => "",
346 }
347 }
348
349 pub(crate) fn pk(&self, name: &str) -> String {
351 format!("{name} BIGINT NOT NULL PRIMARY KEY")
352 }
353}
354
355fn supported(d: &Dialect) -> Result<(), Failure> {
356 match d {
357 Dialect::Postgres | Dialect::Sqlite | Dialect::MySql | Dialect::MariaDb => Ok(()),
358 other => Err(Failure::Backend(RelError::Unsupported(format!(
359 "the relational contract tests have no statements for {other}"
360 )))),
361 }
362}
363
364pub(crate) async fn drop_tables<R: RelationalBackend>(
365 b: &R,
366 names: &[&str],
367) -> Result<(), RelError> {
368 for n in names {
369 b.execute(&format!("DROP TABLE IF EXISTS {n}"), &[]).await?;
370 }
371 Ok(())
372}
373
374pub async fn run_leaf<R: RelationalBackend>(
378 leaf: Leaf,
379 b: &R,
380 constrained: bool,
381) -> Result<(), Failure> {
382 let d = b.dialect();
383 supported(&d)?;
384 let s = Sql(d);
385 match leaf {
386 Leaf::Values => values(b, &s).await,
387 Leaf::Transaction => transaction(b, &s).await,
388 Leaf::UniqueClassified => unique(b, &s).await,
389 Leaf::ForeignKey => foreign_key(b, &s, constrained).await,
390 Leaf::CheckConstraint => check(b, &s, constrained).await,
391 Leaf::IsolationNoDirtyRead => no_dirty_read(b, &s).await,
392 Leaf::IsolationRepeatableRead => repeatable_read(b, &s).await,
393 Leaf::IsolationSerializable => serializable(b, &s).await,
394 Leaf::Savepoint => savepoint(b, &s).await,
395 Leaf::RowLock => row_lock(b, &s).await,
396 Leaf::UpsertWhere => upsert_where(b, &s, constrained).await,
397 }
398}
399
400async fn values<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
401 const T: &str = "persistant_rel_values";
402 drop_tables(b, &[T]).await?;
403 b.execute(
404 &format!(
405 "CREATE TABLE {T} ({}, i {}, t {}, b {}, f {}){}",
406 s.pk("id"),
407 s.ty(Ty::Int),
408 s.ty(Ty::Text),
409 s.ty(Ty::Bytes),
410 s.ty(Ty::Float),
411 s.tail()
412 ),
413 &[],
414 )
415 .await?;
416 let cases: Vec<(i64, Value, Value, Value, Value)> = vec![
417 (
418 1,
419 Value::Int(i64::MIN),
420 Value::Text("ünï ✓ \u{1F980}".to_string()),
421 Value::Bytes(vec![0x00, 0xFF, 0x80, 0x00, 0x7F]),
422 Value::Float(1.5),
423 ),
424 (
425 2,
426 Value::Int(i64::MAX),
427 Value::Text(String::new()),
428 Value::Bytes(vec![]),
429 Value::Float(-0.25),
430 ),
431 (3, Value::Null, Value::Null, Value::Null, Value::Null),
432 ];
433 for (id, i, t, bytes, f) in &cases {
434 b.execute(
435 &format!(
436 "INSERT INTO {T} (id, i, t, b, f) VALUES ({}, {}, {}, {}, {})",
437 s.p(1),
438 s.p(2),
439 s.p(3),
440 s.p(4),
441 s.p(5)
442 ),
443 &[
444 Value::Int(*id),
445 i.clone(),
446 t.clone(),
447 bytes.clone(),
448 f.clone(),
449 ],
450 )
451 .await?;
452 }
453 let rows = b
454 .query(&format!("SELECT id, i, t, b, f FROM {T} ORDER BY id"), &[])
455 .await?;
456 drop_tables(b, &[T]).await?;
457 if rows.len() != cases.len() {
458 return wrong(format!("wrote {} rows, read {}", cases.len(), rows.len()));
459 }
460 for (row, (id, i, t, bytes, f)) in rows.iter().zip(&cases) {
461 let want = [
462 Value::Int(*id),
463 i.clone(),
464 t.clone(),
465 bytes.clone(),
466 f.clone(),
467 ];
468 if row.values != want {
470 return wrong(format!("row {id}: wrote {want:?}, read {:?}", row.values));
471 }
472 }
473 Ok(())
474}
475
476async fn count<R: RelationalBackend>(b: &R, table: &str) -> Result<i64, Failure> {
477 let r = b
478 .query(&format!("SELECT COUNT(*) FROM {table}"), &[])
479 .await?;
480 match r.first().and_then(|r| r.int(0)) {
481 Some(n) => Ok(n),
482 None => wrong("COUNT(*) did not come back as an integer"),
483 }
484}
485
486async fn transaction<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
487 const T: &str = "persistant_rel_tx";
488 drop_tables(b, &[T]).await?;
489 b.execute(
490 &format!(
491 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
492 s.pk("id"),
493 s.tail()
494 ),
495 &[],
496 )
497 .await?;
498 let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
499 let result = async {
500 let mut tx = b.begin(Isolation::ReadCommitted).await?;
502 tx.execute(&ins, &[Value::Int(1), Value::Int(10)]).await?;
503 let own = tx.query(&format!("SELECT COUNT(*) FROM {T}"), &[]).await?;
504 if own.first().and_then(|r| r.int(0)) != Some(1) {
505 return wrong("a transaction cannot see its own write");
506 }
507 tx.commit().await?;
508 if count(b, T).await? != 1 {
509 return wrong("a committed write is not visible");
510 }
511 let mut tx = b.begin(Isolation::ReadCommitted).await?;
513 tx.execute(&ins, &[Value::Int(2), Value::Int(20)]).await?;
514 tx.rollback().await?;
515 if count(b, T).await? != 1 {
516 return wrong("a rolled-back write is visible");
517 }
518 {
520 let mut tx = b.begin(Isolation::ReadCommitted).await?;
521 tx.execute(&ins, &[Value::Int(3), Value::Int(30)]).await?;
522 }
523 if count(b, T).await? != 1 {
524 return wrong("a transaction dropped without commit was not rolled back");
525 }
526 Ok(())
527 }
528 .await;
529 drop_tables(b, &[T]).await?;
530 result
531}
532
533async fn no_dirty_read<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
534 const T: &str = "persistant_rel_isolation";
535 drop_tables(b, &[T]).await?;
536 b.execute(
537 &format!(
538 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
539 s.pk("id"),
540 s.tail()
541 ),
542 &[],
543 )
544 .await?;
545 let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
546 let result = async {
547 let mut tx = b.begin(Isolation::ReadCommitted).await?;
548 tx.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
549 let outside = count(b, T).await?;
551 if outside != 0 {
552 let _ = tx.rollback().await;
553 return wrong(format!(
554 "another connection saw {outside} uncommitted row(s) (dirty read)"
555 ));
556 }
557 tx.commit().await?;
558 if count(b, T).await? != 1 {
559 return wrong("a committed write is not visible to another connection");
560 }
561 Ok(())
562 }
563 .await;
564 drop_tables(b, &[T]).await?;
565 result
566}
567
568fn no_concurrent_commit(s: &Sql) -> Result<(), Failure> {
571 if matches!(s.0, Dialect::Sqlite) {
572 return Err(Failure::Backend(RelError::Unsupported(
573 "begin() is BEGIN IMMEDIATE: it holds the write lock, so no other connection can commit during the transaction and the probe cannot run"
574 .to_string(),
575 )));
576 }
577 Ok(())
578}
579
580async fn finish<T: RelationalTx>(mut tx: T, sql: &str) -> Result<bool, Failure> {
583 match tx.execute(sql, &[]).await {
584 Ok(_) => {}
585 Err(RelError::Serialization) => {
586 let _ = tx.rollback().await;
587 return Ok(false);
588 }
589 Err(e) => {
590 let _ = tx.rollback().await;
591 return Err(e.into());
592 }
593 }
594 match tx.commit().await {
595 Ok(()) => Ok(true),
596 Err(RelError::Serialization) => Ok(false),
597 Err(e) => Err(e.into()),
598 }
599}
600
601async fn write_skew<R: RelationalBackend>(
604 b: &R,
605 s: &Sql,
606 table: &str,
607 level: Isolation,
608) -> Result<(bool, bool), Failure> {
609 b.execute(&format!("DELETE FROM {table}"), &[]).await?;
610 let ins = format!("INSERT INTO {table} (id, n) VALUES ({}, 1)", s.p(1));
611 b.execute(&ins, &[Value::Int(1)]).await?;
612 b.execute(&ins, &[Value::Int(2)]).await?;
613 let read = format!("SELECT COUNT(*) FROM {table} WHERE n = 1");
614 let mut t1 = b.begin(level).await?;
615 let mut t2 = b.begin(level).await?;
616 t1.query(&read, &[]).await?;
617 t2.query(&read, &[]).await?;
618 let c1 = finish(t1, &format!("UPDATE {table} SET n = 0 WHERE id = 1")).await?;
619 let c2 = finish(t2, &format!("UPDATE {table} SET n = 0 WHERE id = 2")).await?;
620 Ok((c1, c2))
621}
622
623async fn serializable<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
624 const T: &str = "persistant_rel_serializable";
625 match s.0 {
626 Dialect::Postgres | Dialect::MySql | Dialect::MariaDb => {}
627 _ => {
628 no_concurrent_commit(s)?;
629 return Err(Failure::Backend(RelError::Unsupported(format!(
630 "no probe for {}",
631 s.0
632 ))));
633 }
634 }
635 drop_tables(b, &[T]).await?;
636 b.execute(
637 &format!(
638 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
639 s.pk("id"),
640 s.tail()
641 ),
642 &[],
643 )
644 .await?;
645 let result = if matches!(s.0, Dialect::Postgres) {
646 async {
647 let control = write_skew(b, s, T, Isolation::ReadCommitted).await?;
649 if control != (true, true) {
650 return wrong(format!(
651 "control: at read committed the write-skew schedule committed {control:?}, not both"
652 ));
653 }
654 let (c1, c2) = write_skew(b, s, T, Isolation::Serializable).await?;
655 if c1 && c2 {
656 return wrong("two serializable transactions that each read both rows and wrote a different one both committed (write skew)");
657 }
658 if !c1 && !c2 {
659 return wrong("both serializable transactions were refused: one should have committed");
660 }
661 Ok(())
662 }
663 .await
664 } else {
665 async {
666 b.execute(
667 &format!("INSERT INTO {T} (id, n) VALUES ({}, 1)", s.p(1)),
668 &[Value::Int(1)],
669 )
670 .await?;
671 let read = format!("SELECT n FROM {T} WHERE id = 1");
672 let nowait = format!("SELECT id FROM {T} WHERE id = 1 FOR UPDATE NOWAIT");
673 let mut rc = b.begin(Isolation::ReadCommitted).await?;
675 rc.query(&read, &[]).await?;
676 let free = b.query(&nowait, &[]).await;
677 let _ = rc.rollback().await;
678 if !matches!(&free, Ok(rows) if rows.len() == 1) {
679 return wrong(format!(
680 "control: a plain read at read committed left the row locked ({free:?})"
681 ));
682 }
683 let mut tx = b.begin(Isolation::Serializable).await?;
684 tx.query(&read, &[]).await?;
685 let held = b.query(&nowait, &[]).await;
686 let _ = tx.rollback().await;
687 if held.is_ok() {
688 return wrong(
689 "a plain read in a serializable transaction did not lock the row it read",
690 );
691 }
692 match b.query(&nowait, &[]).await {
693 Ok(rows) if rows.len() == 1 => Ok(()),
694 other => wrong(format!(
695 "the row is still locked after the serializable transaction ended ({other:?})"
696 )),
697 }
698 }
699 .await
700 };
701 drop_tables(b, &[T]).await?;
702 result
703}
704
705async fn repeatable_read<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
706 const T: &str = "persistant_rel_repeatable";
707 no_concurrent_commit(s)?;
708 drop_tables(b, &[T]).await?;
709 b.execute(
710 &format!(
711 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
712 s.pk("id"),
713 s.tail()
714 ),
715 &[],
716 )
717 .await?;
718 let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
719 let read = format!("SELECT n FROM {T} WHERE id = 1");
720 let result = async {
721 b.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
722 let mut tx = b.begin(Isolation::RepeatableRead).await?;
723 let first = tx.query(&read, &[]).await?;
724 if first.first().and_then(|r| r.int(0)) != Some(1) {
725 let _ = tx.rollback().await;
726 return wrong("the first read did not return the row written before the transaction");
727 }
728 b.execute(
730 &format!("UPDATE {T} SET n = 2 WHERE id = 1"),
731 &[],
732 )
733 .await?;
734 b.execute(&ins, &[Value::Int(2), Value::Int(2)]).await?;
735 let again = tx.query(&read, &[]).await?;
736 let seen = again.first().and_then(|r| r.int(0));
737 if seen != Some(1) {
738 let _ = tx.rollback().await;
739 return wrong(format!(
740 "a row read twice in one transaction changed from 1 to {seen:?} (non-repeatable read)"
741 ));
742 }
743 let rows = tx
744 .query(&format!("SELECT COUNT(*) FROM {T}"), &[])
745 .await?;
746 let n = rows.first().and_then(|r| r.int(0));
747 if n != Some(1) {
748 let _ = tx.rollback().await;
749 return wrong(format!(
750 "a table counted inside one transaction went from 1 to {n:?} rows (phantom read)"
751 ));
752 }
753 tx.commit().await?;
754 let after = b.query(&read, &[]).await?;
755 if after.first().and_then(|r| r.int(0)) != Some(2) {
756 return wrong("the other connection's committed update is not visible after the transaction ends");
757 }
758 Ok(())
759 }
760 .await;
761 drop_tables(b, &[T]).await?;
762 result
763}
764
765async fn savepoint<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
766 const T: &str = "persistant_rel_savepoint";
767 drop_tables(b, &[T]).await?;
768 b.execute(
769 &format!(
770 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
771 s.pk("id"),
772 s.tail()
773 ),
774 &[],
775 )
776 .await?;
777 let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
778 let result = async {
779 let mut tx = b.begin(Isolation::ReadCommitted).await?;
780 tx.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
781 tx.execute("SAVEPOINT persistant_sp", &[]).await?;
782 tx.execute(&ins, &[Value::Int(2), Value::Int(2)]).await?;
783 tx.execute("ROLLBACK TO SAVEPOINT persistant_sp", &[])
784 .await?;
785 tx.commit().await?;
786 let rows = b
787 .query(&format!("SELECT id FROM {T} ORDER BY id"), &[])
788 .await?;
789 let ids: Vec<Option<i64>> = rows.iter().map(|r| r.int(0)).collect();
790 if ids != [Some(1)] {
791 return wrong(format!(
792 "after ROLLBACK TO a savepoint and commit, expected only row 1, found {ids:?}"
793 ));
794 }
795 Ok(())
796 }
797 .await;
798 drop_tables(b, &[T]).await?;
799 result
800}
801
802async fn row_lock<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
803 const T: &str = "persistant_rel_row_lock";
804 if matches!(s.0, Dialect::Sqlite) {
805 return Err(Failure::Backend(RelError::Unsupported(
806 "SQLite has no row locks: a write locks the whole database".to_string(),
807 )));
808 }
809 drop_tables(b, &[T]).await?;
810 b.execute(
811 &format!(
812 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
813 s.pk("id"),
814 s.tail()
815 ),
816 &[],
817 )
818 .await?;
819 b.execute(
820 &format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2)),
821 &[Value::Int(1), Value::Int(1)],
822 )
823 .await?;
824 let lock = format!("SELECT id FROM {T} WHERE id = 1 FOR UPDATE");
825 let probe = format!("SELECT id FROM {T} WHERE id = 1 FOR UPDATE SKIP LOCKED");
826 let result = async {
827 let mut tx = b.begin(Isolation::ReadCommitted).await?;
828 if tx.query(&lock, &[]).await?.len() != 1 {
829 return wrong("FOR UPDATE did not return the row it locks");
830 }
831 let held = b.query(&probe, &[]).await;
833 let held = match held {
834 Ok(rows) => rows.len(),
835 Err(e) => {
836 let _ = tx.rollback().await;
837 return Err(e.into());
838 }
839 };
840 if held != 0 {
841 let _ = tx.rollback().await;
842 return wrong("another connection locked a row an open transaction holds FOR UPDATE");
843 }
844 tx.commit().await?;
845 if b.query(&probe, &[]).await?.len() != 1 {
846 return wrong("the row is still locked after the transaction committed");
847 }
848 Ok(())
849 }
850 .await;
851 drop_tables(b, &[T]).await?;
852 result
853}
854
855async fn upsert_where<R: RelationalBackend>(
857 b: &R,
858 s: &Sql,
859 constrained: bool,
860) -> Result<(), Failure> {
861 const T: &str = "persistant_rel_upsert";
862 drop_tables(b, &[T]).await?;
863 b.execute(
864 &format!(
865 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
866 s.pk("id"),
867 s.tail()
868 ),
869 &[],
870 )
871 .await?;
872 let up = if matches!(s.0, Dialect::MySql | Dialect::MariaDb) {
874 let set = if constrained {
875 format!("IF({T}.n < VALUES(n), VALUES(n), {T}.n)")
876 } else {
877 "VALUES(n)".to_string()
878 };
879 format!(
880 "INSERT INTO {T} (id, n) VALUES ({}, {}) ON DUPLICATE KEY UPDATE n = {set}",
881 s.p(1),
882 s.p(2)
883 )
884 } else {
885 let cond = if constrained {
886 format!(" WHERE {T}.n < EXCLUDED.n")
887 } else {
888 String::new()
889 };
890 format!(
891 "INSERT INTO {T} (id, n) VALUES ({}, {}) ON CONFLICT (id) DO UPDATE SET n = EXCLUDED.n{cond}",
892 s.p(1),
893 s.p(2)
894 )
895 };
896 let result = async {
897 for (n, want) in [(10, 10), (5, 10), (20, 20)] {
899 b.execute(&up, &[Value::Int(1), Value::Int(n)]).await?;
900 let rows = b
901 .query(&format!("SELECT n FROM {T} WHERE id = 1"), &[])
902 .await?;
903 let got = rows.first().and_then(|r| r.int(0));
904 if got != Some(want) {
905 return wrong(format!(
906 "upsert of {n} left {got:?}, expected {want} (the WHERE must hold the row when n is not higher)"
907 ));
908 }
909 }
910 Ok(())
911 }
912 .await;
913 drop_tables(b, &[T]).await?;
914 result
915}
916
917async fn unique<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
918 const T: &str = "persistant_rel_unique";
919 drop_tables(b, &[T]).await?;
920 b.execute(
921 &format!(
922 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
923 s.pk("id"),
924 s.tail()
925 ),
926 &[],
927 )
928 .await?;
929 let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
930 let result = async {
931 b.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
932 match b.execute(&ins, &[Value::Int(1), Value::Int(2)]).await {
933 Err(RelError::UniqueViolation { .. }) => {}
934 other => return wrong(format!("a duplicate key came back as {other:?}")),
935 }
936 let mut tx = b.begin(Isolation::ReadCommitted).await?;
938 let dup = tx.execute(&ins, &[Value::Int(1), Value::Int(3)]).await;
939 let _ = tx.rollback().await;
940 match dup {
941 Err(RelError::UniqueViolation { .. }) => Ok(()),
942 other => wrong(format!(
943 "a duplicate key in a transaction came back as {other:?}"
944 )),
945 }
946 }
947 .await;
948 drop_tables(b, &[T]).await?;
949 result
950}
951
952async fn foreign_key<R: RelationalBackend>(
953 b: &R,
954 s: &Sql,
955 constrained: bool,
956) -> Result<(), Failure> {
957 const P: &str = "persistant_rel_fk_parent";
958 const C: &str = "persistant_rel_fk_child";
959 drop_tables(b, &[C, P]).await?;
960 b.execute(
961 &format!("CREATE TABLE {P} ({}){}", s.pk("id"), s.tail()),
962 &[],
963 )
964 .await?;
965 let refs = if constrained {
966 format!(", FOREIGN KEY (pid) REFERENCES {P}(id)")
967 } else {
968 String::new()
969 };
970 b.execute(
971 &format!(
972 "CREATE TABLE {C} ({}, pid BIGINT NOT NULL{refs}){}",
973 s.pk("id"),
974 s.tail()
975 ),
976 &[],
977 )
978 .await?;
979 let result = async {
980 b.execute(
981 &format!("INSERT INTO {P} (id) VALUES ({})", s.p(1)),
982 &[Value::Int(1)],
983 )
984 .await?;
985 let ins = format!("INSERT INTO {C} (id, pid) VALUES ({}, {})", s.p(1), s.p(2));
986 b.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
987 match b.execute(&ins, &[Value::Int(2), Value::Int(99)]).await {
988 Err(RelError::ForeignKeyViolation { .. }) => {}
989 Ok(_) => return wrong("a reference to a missing row was accepted"),
990 other => {
991 return wrong(format!(
992 "a reference to a missing row came back as {other:?}"
993 ));
994 }
995 }
996 match b
997 .execute(
998 &format!("DELETE FROM {P} WHERE id = {}", s.p(1)),
999 &[Value::Int(1)],
1000 )
1001 .await
1002 {
1003 Err(RelError::ForeignKeyViolation { .. }) => Ok(()),
1004 Ok(_) => wrong("a referenced row was deleted"),
1005 other => wrong(format!("deleting a referenced row came back as {other:?}")),
1006 }
1007 }
1008 .await;
1009 drop_tables(b, &[C, P]).await?;
1010 result
1011}
1012
1013async fn check<R: RelationalBackend>(b: &R, s: &Sql, constrained: bool) -> Result<(), Failure> {
1014 const T: &str = "persistant_rel_check";
1015 drop_tables(b, &[T]).await?;
1016 let ck = if constrained { " CHECK (n >= 0)" } else { "" };
1017 b.execute(
1018 &format!(
1019 "CREATE TABLE {T} ({}, n BIGINT NOT NULL{ck}){}",
1020 s.pk("id"),
1021 s.tail()
1022 ),
1023 &[],
1024 )
1025 .await?;
1026 let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
1027 let result = async {
1028 b.execute(&ins, &[Value::Int(1), Value::Int(0)]).await?;
1029 match b.execute(&ins, &[Value::Int(2), Value::Int(-1)]).await {
1030 Err(RelError::CheckViolation { .. }) => Ok(()),
1031 Ok(_) => wrong("a row violating a CHECK was accepted"),
1032 other => wrong(format!("a row violating a CHECK came back as {other:?}")),
1033 }
1034 }
1035 .await;
1036 drop_tables(b, &[T]).await?;
1037 result
1038}
1039
1040#[derive(Debug, Clone, PartialEq, Eq)]
1044pub struct Certified {
1045 pub leaf: Leaf,
1047 pub row: MatrixRow,
1049}
1050
1051pub async fn certify<R: RelationalBackend>(
1057 target: &R,
1058 config: &Config,
1059 metadata: &Config,
1060) -> Vec<Certified> {
1061 certify_leaves(target, config, metadata, &Leaf::ALL).await
1062}
1063
1064pub async fn certify_leaves<R: RelationalBackend>(
1066 target: &R,
1067 config: &Config,
1068 metadata: &Config,
1069 leaves: &[Leaf],
1070) -> Vec<Certified> {
1071 let mut out = Vec::new();
1072 for leaf in leaves.iter().copied() {
1073 let result = run_leaf(leaf, target, true).await;
1074 let positive_passed = !matches!(
1075 &result,
1076 Err(Failure::Backend(
1077 RelError::Unsupported(_) | RelError::Unavailable(_)
1078 ))
1079 );
1080 let mut known_bad_failed = true;
1081 for d in leaf.defects() {
1082 let bad = Defective::new(target, *d);
1083 if run_leaf(leaf, &bad, true).await.is_ok() {
1084 known_bad_failed = false;
1085 }
1086 }
1087 if leaf.has_unconstrained_control() && run_leaf(leaf, target, false).await.is_ok() {
1088 known_bad_failed = false;
1089 }
1090 let controls = Controls {
1091 positive_passed,
1092 known_bad_failed,
1093 };
1094 let id = CapabilityId::new(leaf.id()).expect("leaf id is valid");
1095 let md = metadata.clone().with("test", format!("rel::{}", leaf.id()));
1096 let off = |reason: Reason, note: String| MatrixRow {
1097 subject_kind: "capability".to_string(),
1098 subject: id.clone(),
1099 backend: target.backend().to_string(),
1100 version: target.version().to_string(),
1101 config: config.clone(),
1102 enabled: false,
1103 reason: Some(reason),
1104 metadata: md.clone(),
1105 note: note.replace(['\t', '\n'], " "),
1106 extra: BTreeMap::new(),
1107 };
1108 let row = match (
1109 &result,
1110 controls.positive_passed && controls.known_bad_failed,
1111 ) {
1112 (Ok(()), true) => Verdict::Exact
1113 .into_row(
1114 id.clone(),
1115 target.backend(),
1116 target.version(),
1117 config.clone(),
1118 md.clone(),
1119 controls,
1120 )
1121 .expect("both controls hold"),
1122 (Ok(()), false) => off(
1123 Reason::Untested,
1124 format!("controls did not hold: {controls:?}"),
1125 ),
1126 (Err(Failure::Wrong(m)), _) => off(Reason::Wrong, m.clone()),
1127 (Err(Failure::Backend(e)), _) => off(Reason::Missing, e.to_string()),
1128 };
1129 out.push(Certified { leaf, row });
1130 }
1131 out
1132}