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