Skip to main content

persistant_relational/
conformance.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Contract tests for the relational kind: what [`RelationalBackend`] promises, decided against a real
3//! database. Each leaf produces a matrix row, **on** only when the controls held.
4//!
5//! - `rel.values`: integers, text, bytes and floats written as parameters come back as written, and a
6//!   `NULL` comes back as `Value::Null`.
7//! - `rel.transaction`: a committed write is visible, a rolled-back one is not, and a transaction
8//!   dropped without `commit` rolls back.
9//! - `rel.error.unique`: a duplicate key is reported as [`RelError::UniqueViolation`], not as an
10//!   opaque backend error.
11//! - `rel.foreign_key`: a reference to a missing row is rejected as [`RelError::ForeignKeyViolation`].
12//! - `rel.check_constraint`: a row violating a `CHECK` is rejected as [`RelError::CheckViolation`].
13//! - `rel.isolation.no_dirty_read`: a write inside an open transaction is invisible to another
14//!   connection until it commits, and visible after (read committed or stronger).
15//! - `rel.row_lock`: a row locked `FOR UPDATE` by an open transaction cannot be locked by another connection
16//!   (probed with `SKIP LOCKED`, which skips it) until the transaction ends. SQLite has no row locks, so it is off there.
17//! - `rel.savepoint`: inside a transaction, `ROLLBACK TO` a savepoint undoes the work after it and keeps the work before it.
18//!
19//! **Controls.** There is no reference relational engine to use as a positive control, so the positive
20//! control is weaker than in the kv and blob suites: it is that the leaf's schema could be created on
21//! the target at all (a failure to create it is "missing", not "wrong"). The **known-bad** controls are
22//! real: a [`Defect`] wrapper that makes the same target misbehave in one way (no transactions, lossy
23//! bytes, unclassified errors), and, for the two constraint leaves, the same test run against a schema
24//! created *without* the constraint, which must be caught accepting the bad row.
25//!
26//! Statements are written per dialect (this is a probe of the dialect itself, not of translation).
27//! Booleans are left out of `rel.values`: how a driver returns one (`BOOL`, `TINYINT(1)`, `0/1`) is a
28//! documented difference, not a contract.
29
30use crate::{Dialect, RelError, RelationalBackend, RelationalTx, Row, Value};
31use persistant_core::{CapabilityId, Config, Controls, Reason, Row as MatrixRow, Verdict};
32use std::collections::BTreeMap;
33
34/// A leaf this module can decide.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36#[non_exhaustive]
37pub enum Leaf {
38    /// `rel.values`
39    Values,
40    /// `rel.transaction`
41    Transaction,
42    /// `rel.error.unique`
43    UniqueClassified,
44    /// `rel.foreign_key`
45    ForeignKey,
46    /// `rel.check_constraint`
47    CheckConstraint,
48    /// `rel.isolation.no_dirty_read`
49    IsolationNoDirtyRead,
50    /// `rel.savepoint`
51    Savepoint,
52    /// `rel.row_lock`
53    RowLock,
54}
55
56impl Leaf {
57    /// Every leaf with a test here.
58    pub const ALL: [Leaf; 8] = [
59        Leaf::Values,
60        Leaf::Transaction,
61        Leaf::UniqueClassified,
62        Leaf::ForeignKey,
63        Leaf::CheckConstraint,
64        Leaf::IsolationNoDirtyRead,
65        Leaf::Savepoint,
66        Leaf::RowLock,
67    ];
68
69    /// The capability id.
70    pub fn id(self) -> &'static str {
71        match self {
72            Leaf::Values => "rel.values",
73            Leaf::Transaction => "rel.transaction",
74            Leaf::UniqueClassified => "rel.error.unique",
75            Leaf::ForeignKey => "rel.foreign_key",
76            Leaf::CheckConstraint => "rel.check_constraint",
77            Leaf::IsolationNoDirtyRead => "rel.isolation.no_dirty_read",
78            Leaf::Savepoint => "rel.savepoint",
79            Leaf::RowLock => "rel.row_lock",
80        }
81    }
82
83    fn defects(self) -> &'static [Defect] {
84        match self {
85            Leaf::Values => &[Defect::LossyBytes],
86            Leaf::Transaction | Leaf::IsolationNoDirtyRead | Leaf::Savepoint | Leaf::RowLock => {
87                &[Defect::NoTransactions]
88            }
89            Leaf::UniqueClassified => &[Defect::UnclassifiedErrors],
90            Leaf::ForeignKey | Leaf::CheckConstraint => &[],
91        }
92    }
93
94    /// Whether the leaf has a "schema without the constraint" known-bad run.
95    fn has_unconstrained_control(self) -> bool {
96        matches!(self, Leaf::ForeignKey | Leaf::CheckConstraint)
97    }
98}
99
100/// One way for a wrapped backend to be wrong, for the known-bad controls.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102#[non_exhaustive]
103pub enum Defect {
104    /// `begin` returns a transaction whose statements run at once and whose `rollback` and drop do
105    /// nothing.
106    NoTransactions,
107    /// A byte parameter is cut to its first byte.
108    LossyBytes,
109    /// Every error comes back as `RelError::Backend`.
110    UnclassifiedErrors,
111}
112
113/// A backend with one [`Defect`] injected. The wrapped backend does the work.
114#[derive(Debug)]
115pub struct Defective<'a, R> {
116    inner: &'a R,
117    defect: Defect,
118}
119
120impl<'a, R: RelationalBackend> Defective<'a, R> {
121    /// Wrap `inner` so it is wrong in exactly the way `defect` says.
122    pub fn new(inner: &'a R, defect: Defect) -> Self {
123        Defective { inner, defect }
124    }
125}
126
127/// The transaction of a [`Defective`] backend.
128pub enum DefectiveTx<'a, R: RelationalBackend + 'a> {
129    /// The real transaction (the defect is elsewhere).
130    Real(R::Tx<'a>, Defect),
131    /// No transaction at all: statements run on the backend directly.
132    Direct(&'a R),
133}
134
135impl<R: RelationalBackend> RelationalBackend for Defective<'_, R> {
136    type Tx<'a>
137        = DefectiveTx<'a, R>
138    where
139        Self: 'a;
140
141    fn backend(&self) -> &str {
142        self.inner.backend()
143    }
144
145    fn version(&self) -> &str {
146        self.inner.version()
147    }
148
149    fn dialect(&self) -> Dialect {
150        self.inner.dialect()
151    }
152
153    async fn execute(&self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
154        self.inner
155            .execute(sql, &lossy(self.defect, params))
156            .await
157            .map_err(|e| unclassify(self.defect, e))
158    }
159
160    async fn query(&self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
161        self.inner
162            .query(sql, &lossy(self.defect, params))
163            .await
164            .map_err(|e| unclassify(self.defect, e))
165    }
166
167    async fn begin(&self) -> Result<DefectiveTx<'_, R>, RelError> {
168        if self.defect == Defect::NoTransactions {
169            return Ok(DefectiveTx::Direct(self.inner));
170        }
171        Ok(DefectiveTx::Real(
172            self.inner
173                .begin()
174                .await
175                .map_err(|e| unclassify(self.defect, e))?,
176            self.defect,
177        ))
178    }
179}
180
181fn lossy(defect: Defect, params: &[Value]) -> Vec<Value> {
182    params
183        .iter()
184        .map(|p| match (defect, p) {
185            (Defect::LossyBytes, Value::Bytes(b)) => {
186                Value::Bytes(b.iter().take(1).copied().collect())
187            }
188            _ => p.clone(),
189        })
190        .collect()
191}
192
193fn unclassify(defect: Defect, e: RelError) -> RelError {
194    if defect == Defect::UnclassifiedErrors {
195        RelError::Backend(e.to_string())
196    } else {
197        e
198    }
199}
200
201impl<R: RelationalBackend> RelationalTx for DefectiveTx<'_, R> {
202    async fn execute(&mut self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
203        match self {
204            DefectiveTx::Real(tx, d) => tx
205                .execute(sql, &lossy(*d, params))
206                .await
207                .map_err(|e| unclassify(*d, e)),
208            DefectiveTx::Direct(db) => db.execute(sql, params).await,
209        }
210    }
211
212    async fn query(&mut self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
213        match self {
214            DefectiveTx::Real(tx, d) => tx
215                .query(sql, &lossy(*d, params))
216                .await
217                .map_err(|e| unclassify(*d, e)),
218            DefectiveTx::Direct(db) => db.query(sql, params).await,
219        }
220    }
221
222    async fn commit(self) -> Result<(), RelError> {
223        match self {
224            DefectiveTx::Real(tx, d) => tx.commit().await.map_err(|e| unclassify(d, e)),
225            DefectiveTx::Direct(_) => Ok(()),
226        }
227    }
228
229    async fn rollback(self) -> Result<(), RelError> {
230        match self {
231            DefectiveTx::Real(tx, d) => tx.rollback().await.map_err(|e| unclassify(d, e)),
232            DefectiveTx::Direct(_) => Ok(()),
233        }
234    }
235}
236
237/// Why a leaf's test did not pass.
238#[derive(Debug, Clone, PartialEq, Eq)]
239pub enum Failure {
240    /// The backend returned something other than the contract says.
241    Wrong(String),
242    /// The backend could not do something the test needs (including creating the leaf's schema).
243    Backend(RelError),
244}
245
246impl From<RelError> for Failure {
247    fn from(e: RelError) -> Self {
248        Failure::Backend(e)
249    }
250}
251
252fn wrong<T>(msg: impl Into<String>) -> Result<T, Failure> {
253    Err(Failure::Wrong(msg.into()))
254}
255
256// ---- per-dialect statement text -------------------------------------------------------------
257
258#[derive(Clone, Copy)]
259enum Ty {
260    Int,
261    Text,
262    Bytes,
263    Float,
264}
265
266struct Sql(Dialect);
267
268impl Sql {
269    /// The placeholder for parameter `n` (1-based).
270    fn p(&self, n: usize) -> String {
271        match self.0 {
272            Dialect::Postgres => format!("${n}"),
273            Dialect::Sqlite => format!("?{n}"),
274            _ => "?".to_string(),
275        }
276    }
277
278    fn ty(&self, t: Ty) -> &'static str {
279        match (&self.0, t) {
280            (_, Ty::Int) => "BIGINT",
281            (Dialect::Postgres, Ty::Text) => "TEXT",
282            (Dialect::Sqlite, Ty::Text) => "TEXT",
283            (_, Ty::Text) => "VARCHAR(255)",
284            (Dialect::Postgres, Ty::Bytes) => "BYTEA",
285            (Dialect::Sqlite, Ty::Bytes) => "BLOB",
286            (_, Ty::Bytes) => "LONGBLOB",
287            (Dialect::Postgres, Ty::Float) => "DOUBLE PRECISION",
288            (_, Ty::Float) => "DOUBLE",
289        }
290    }
291
292    /// Table options (InnoDB is explicit on MySQL so constraints and transactions exist).
293    fn tail(&self) -> &'static str {
294        match self.0 {
295            Dialect::MySql | Dialect::MariaDb => " ENGINE=InnoDB",
296            _ => "",
297        }
298    }
299
300    /// An integer primary key column definition.
301    fn pk(&self, name: &str) -> String {
302        format!("{name} BIGINT NOT NULL PRIMARY KEY")
303    }
304}
305
306fn supported(d: &Dialect) -> Result<(), Failure> {
307    match d {
308        Dialect::Postgres | Dialect::Sqlite | Dialect::MySql | Dialect::MariaDb => Ok(()),
309        other => Err(Failure::Backend(RelError::Unsupported(format!(
310            "the relational contract tests have no statements for {other}"
311        )))),
312    }
313}
314
315async fn drop_tables<R: RelationalBackend>(b: &R, names: &[&str]) -> Result<(), RelError> {
316    for n in names {
317        b.execute(&format!("DROP TABLE IF EXISTS {n}"), &[]).await?;
318    }
319    Ok(())
320}
321
322/// Run one leaf's test against `b`. `constrained = false` creates the constraint leaves' schema without
323/// the constraint (the known-bad control); it makes no difference to the other leaves. Writes only to
324/// tables named `persistant_rel_*`, which it drops first and last.
325pub async fn run_leaf<R: RelationalBackend>(
326    leaf: Leaf,
327    b: &R,
328    constrained: bool,
329) -> Result<(), Failure> {
330    let d = b.dialect();
331    supported(&d)?;
332    let s = Sql(d);
333    match leaf {
334        Leaf::Values => values(b, &s).await,
335        Leaf::Transaction => transaction(b, &s).await,
336        Leaf::UniqueClassified => unique(b, &s).await,
337        Leaf::ForeignKey => foreign_key(b, &s, constrained).await,
338        Leaf::CheckConstraint => check(b, &s, constrained).await,
339        Leaf::IsolationNoDirtyRead => no_dirty_read(b, &s).await,
340        Leaf::Savepoint => savepoint(b, &s).await,
341        Leaf::RowLock => row_lock(b, &s).await,
342    }
343}
344
345async fn values<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
346    const T: &str = "persistant_rel_values";
347    drop_tables(b, &[T]).await?;
348    b.execute(
349        &format!(
350            "CREATE TABLE {T} ({}, i {}, t {}, b {}, f {}){}",
351            s.pk("id"),
352            s.ty(Ty::Int),
353            s.ty(Ty::Text),
354            s.ty(Ty::Bytes),
355            s.ty(Ty::Float),
356            s.tail()
357        ),
358        &[],
359    )
360    .await?;
361    let cases: Vec<(i64, Value, Value, Value, Value)> = vec![
362        (
363            1,
364            Value::Int(i64::MIN),
365            Value::Text("ünï ✓ \u{1F980}".to_string()),
366            Value::Bytes(vec![0x00, 0xFF, 0x80, 0x00, 0x7F]),
367            Value::Float(1.5),
368        ),
369        (
370            2,
371            Value::Int(i64::MAX),
372            Value::Text(String::new()),
373            Value::Bytes(vec![]),
374            Value::Float(-0.25),
375        ),
376        (3, Value::Null, Value::Null, Value::Null, Value::Null),
377    ];
378    for (id, i, t, bytes, f) in &cases {
379        b.execute(
380            &format!(
381                "INSERT INTO {T} (id, i, t, b, f) VALUES ({}, {}, {}, {}, {})",
382                s.p(1),
383                s.p(2),
384                s.p(3),
385                s.p(4),
386                s.p(5)
387            ),
388            &[
389                Value::Int(*id),
390                i.clone(),
391                t.clone(),
392                bytes.clone(),
393                f.clone(),
394            ],
395        )
396        .await?;
397    }
398    let rows = b
399        .query(&format!("SELECT id, i, t, b, f FROM {T} ORDER BY id"), &[])
400        .await?;
401    drop_tables(b, &[T]).await?;
402    if rows.len() != cases.len() {
403        return wrong(format!("wrote {} rows, read {}", cases.len(), rows.len()));
404    }
405    for (row, (id, i, t, bytes, f)) in rows.iter().zip(&cases) {
406        let want = [
407            Value::Int(*id),
408            i.clone(),
409            t.clone(),
410            bytes.clone(),
411            f.clone(),
412        ];
413        // An empty byte string and an empty text may come back as NULL on no engine here; they must not.
414        if row.values != want {
415            return wrong(format!("row {id}: wrote {want:?}, read {:?}", row.values));
416        }
417    }
418    Ok(())
419}
420
421async fn count<R: RelationalBackend>(b: &R, table: &str) -> Result<i64, Failure> {
422    let r = b
423        .query(&format!("SELECT COUNT(*) FROM {table}"), &[])
424        .await?;
425    match r.first().and_then(|r| r.int(0)) {
426        Some(n) => Ok(n),
427        None => wrong("COUNT(*) did not come back as an integer"),
428    }
429}
430
431async fn transaction<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
432    const T: &str = "persistant_rel_tx";
433    drop_tables(b, &[T]).await?;
434    b.execute(
435        &format!(
436            "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
437            s.pk("id"),
438            s.tail()
439        ),
440        &[],
441    )
442    .await?;
443    let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
444    let result = async {
445        // Commit: visible.
446        let mut tx = b.begin().await?;
447        tx.execute(&ins, &[Value::Int(1), Value::Int(10)]).await?;
448        let own = tx.query(&format!("SELECT COUNT(*) FROM {T}"), &[]).await?;
449        if own.first().and_then(|r| r.int(0)) != Some(1) {
450            return wrong("a transaction cannot see its own write");
451        }
452        tx.commit().await?;
453        if count(b, T).await? != 1 {
454            return wrong("a committed write is not visible");
455        }
456        // Rollback: not visible.
457        let mut tx = b.begin().await?;
458        tx.execute(&ins, &[Value::Int(2), Value::Int(20)]).await?;
459        tx.rollback().await?;
460        if count(b, T).await? != 1 {
461            return wrong("a rolled-back write is visible");
462        }
463        // Dropped without commit: rolled back.
464        {
465            let mut tx = b.begin().await?;
466            tx.execute(&ins, &[Value::Int(3), Value::Int(30)]).await?;
467        }
468        if count(b, T).await? != 1 {
469            return wrong("a transaction dropped without commit was not rolled back");
470        }
471        Ok(())
472    }
473    .await;
474    drop_tables(b, &[T]).await?;
475    result
476}
477
478async fn no_dirty_read<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
479    const T: &str = "persistant_rel_isolation";
480    drop_tables(b, &[T]).await?;
481    b.execute(
482        &format!(
483            "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
484            s.pk("id"),
485            s.tail()
486        ),
487        &[],
488    )
489    .await?;
490    let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
491    let result = async {
492        let mut tx = b.begin().await?;
493        tx.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
494        // `b.query` runs on another connection, outside `tx`.
495        let outside = count(b, T).await?;
496        if outside != 0 {
497            let _ = tx.rollback().await;
498            return wrong(format!(
499                "another connection saw {outside} uncommitted row(s) (dirty read)"
500            ));
501        }
502        tx.commit().await?;
503        if count(b, T).await? != 1 {
504            return wrong("a committed write is not visible to another connection");
505        }
506        Ok(())
507    }
508    .await;
509    drop_tables(b, &[T]).await?;
510    result
511}
512
513async fn savepoint<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
514    const T: &str = "persistant_rel_savepoint";
515    drop_tables(b, &[T]).await?;
516    b.execute(
517        &format!(
518            "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
519            s.pk("id"),
520            s.tail()
521        ),
522        &[],
523    )
524    .await?;
525    let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
526    let result = async {
527        let mut tx = b.begin().await?;
528        tx.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
529        tx.execute("SAVEPOINT persistant_sp", &[]).await?;
530        tx.execute(&ins, &[Value::Int(2), Value::Int(2)]).await?;
531        tx.execute("ROLLBACK TO SAVEPOINT persistant_sp", &[])
532            .await?;
533        tx.commit().await?;
534        let rows = b
535            .query(&format!("SELECT id FROM {T} ORDER BY id"), &[])
536            .await?;
537        let ids: Vec<Option<i64>> = rows.iter().map(|r| r.int(0)).collect();
538        if ids != [Some(1)] {
539            return wrong(format!(
540                "after ROLLBACK TO a savepoint and commit, expected only row 1, found {ids:?}"
541            ));
542        }
543        Ok(())
544    }
545    .await;
546    drop_tables(b, &[T]).await?;
547    result
548}
549
550async fn row_lock<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
551    const T: &str = "persistant_rel_row_lock";
552    if matches!(s.0, Dialect::Sqlite) {
553        return Err(Failure::Backend(RelError::Unsupported(
554            "SQLite has no row locks: a write locks the whole database".to_string(),
555        )));
556    }
557    drop_tables(b, &[T]).await?;
558    b.execute(
559        &format!(
560            "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
561            s.pk("id"),
562            s.tail()
563        ),
564        &[],
565    )
566    .await?;
567    b.execute(
568        &format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2)),
569        &[Value::Int(1), Value::Int(1)],
570    )
571    .await?;
572    let lock = format!("SELECT id FROM {T} WHERE id = 1 FOR UPDATE");
573    let probe = format!("SELECT id FROM {T} WHERE id = 1 FOR UPDATE SKIP LOCKED");
574    let result = async {
575        let mut tx = b.begin().await?;
576        if tx.query(&lock, &[]).await?.len() != 1 {
577            return wrong("FOR UPDATE did not return the row it locks");
578        }
579        // `b.query` runs on another connection, outside `tx`.
580        let held = b.query(&probe, &[]).await;
581        let held = match held {
582            Ok(rows) => rows.len(),
583            Err(e) => {
584                let _ = tx.rollback().await;
585                return Err(e.into());
586            }
587        };
588        if held != 0 {
589            let _ = tx.rollback().await;
590            return wrong("another connection locked a row an open transaction holds FOR UPDATE");
591        }
592        tx.commit().await?;
593        if b.query(&probe, &[]).await?.len() != 1 {
594            return wrong("the row is still locked after the transaction committed");
595        }
596        Ok(())
597    }
598    .await;
599    drop_tables(b, &[T]).await?;
600    result
601}
602
603async fn unique<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
604    const T: &str = "persistant_rel_unique";
605    drop_tables(b, &[T]).await?;
606    b.execute(
607        &format!(
608            "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
609            s.pk("id"),
610            s.tail()
611        ),
612        &[],
613    )
614    .await?;
615    let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
616    let result = async {
617        b.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
618        match b.execute(&ins, &[Value::Int(1), Value::Int(2)]).await {
619            Err(RelError::UniqueViolation { .. }) => {}
620            other => return wrong(format!("a duplicate key came back as {other:?}")),
621        }
622        // Inside a transaction too.
623        let mut tx = b.begin().await?;
624        let dup = tx.execute(&ins, &[Value::Int(1), Value::Int(3)]).await;
625        let _ = tx.rollback().await;
626        match dup {
627            Err(RelError::UniqueViolation { .. }) => Ok(()),
628            other => wrong(format!(
629                "a duplicate key in a transaction came back as {other:?}"
630            )),
631        }
632    }
633    .await;
634    drop_tables(b, &[T]).await?;
635    result
636}
637
638async fn foreign_key<R: RelationalBackend>(
639    b: &R,
640    s: &Sql,
641    constrained: bool,
642) -> Result<(), Failure> {
643    const P: &str = "persistant_rel_fk_parent";
644    const C: &str = "persistant_rel_fk_child";
645    drop_tables(b, &[C, P]).await?;
646    b.execute(
647        &format!("CREATE TABLE {P} ({}){}", s.pk("id"), s.tail()),
648        &[],
649    )
650    .await?;
651    let refs = if constrained {
652        format!(", FOREIGN KEY (pid) REFERENCES {P}(id)")
653    } else {
654        String::new()
655    };
656    b.execute(
657        &format!(
658            "CREATE TABLE {C} ({}, pid BIGINT NOT NULL{refs}){}",
659            s.pk("id"),
660            s.tail()
661        ),
662        &[],
663    )
664    .await?;
665    let result = async {
666        b.execute(
667            &format!("INSERT INTO {P} (id) VALUES ({})", s.p(1)),
668            &[Value::Int(1)],
669        )
670        .await?;
671        let ins = format!("INSERT INTO {C} (id, pid) VALUES ({}, {})", s.p(1), s.p(2));
672        b.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
673        match b.execute(&ins, &[Value::Int(2), Value::Int(99)]).await {
674            Err(RelError::ForeignKeyViolation { .. }) => {}
675            Ok(_) => return wrong("a reference to a missing row was accepted"),
676            other => {
677                return wrong(format!(
678                    "a reference to a missing row came back as {other:?}"
679                ));
680            }
681        }
682        match b
683            .execute(
684                &format!("DELETE FROM {P} WHERE id = {}", s.p(1)),
685                &[Value::Int(1)],
686            )
687            .await
688        {
689            Err(RelError::ForeignKeyViolation { .. }) => Ok(()),
690            Ok(_) => wrong("a referenced row was deleted"),
691            other => wrong(format!("deleting a referenced row came back as {other:?}")),
692        }
693    }
694    .await;
695    drop_tables(b, &[C, P]).await?;
696    result
697}
698
699async fn check<R: RelationalBackend>(b: &R, s: &Sql, constrained: bool) -> Result<(), Failure> {
700    const T: &str = "persistant_rel_check";
701    drop_tables(b, &[T]).await?;
702    let ck = if constrained { " CHECK (n >= 0)" } else { "" };
703    b.execute(
704        &format!(
705            "CREATE TABLE {T} ({}, n BIGINT NOT NULL{ck}){}",
706            s.pk("id"),
707            s.tail()
708        ),
709        &[],
710    )
711    .await?;
712    let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
713    let result = async {
714        b.execute(&ins, &[Value::Int(1), Value::Int(0)]).await?;
715        match b.execute(&ins, &[Value::Int(2), Value::Int(-1)]).await {
716            Err(RelError::CheckViolation { .. }) => Ok(()),
717            Ok(_) => wrong("a row violating a CHECK was accepted"),
718            other => wrong(format!("a row violating a CHECK came back as {other:?}")),
719        }
720    }
721    .await;
722    drop_tables(b, &[T]).await?;
723    result
724}
725
726// ---- certification --------------------------------------------------------------------------
727
728/// The outcome of certifying one leaf.
729#[derive(Debug, Clone, PartialEq, Eq)]
730pub struct Certified {
731    /// The leaf.
732    pub leaf: Leaf,
733    /// The row to record.
734    pub row: MatrixRow,
735}
736
737/// Run every leaf here against `target` and return one row per leaf.
738///
739/// A leaf is **on** only if the target passed *and* every known-bad control failed (see the module
740/// docs for the positive control). `config` is the facts a gate can read for this target; `metadata`
741/// is copied into every row with `test=rel::<leaf>` added.
742pub async fn certify<R: RelationalBackend>(
743    target: &R,
744    config: &Config,
745    metadata: &Config,
746) -> Vec<Certified> {
747    let mut out = Vec::new();
748    for leaf in Leaf::ALL {
749        let result = run_leaf(leaf, target, true).await;
750        let positive_passed = !matches!(
751            &result,
752            Err(Failure::Backend(
753                RelError::Unsupported(_) | RelError::Unavailable(_)
754            ))
755        );
756        let mut known_bad_failed = true;
757        for d in leaf.defects() {
758            let bad = Defective::new(target, *d);
759            if run_leaf(leaf, &bad, true).await.is_ok() {
760                known_bad_failed = false;
761            }
762        }
763        if leaf.has_unconstrained_control() && run_leaf(leaf, target, false).await.is_ok() {
764            known_bad_failed = false;
765        }
766        let controls = Controls {
767            positive_passed,
768            known_bad_failed,
769        };
770        let id = CapabilityId::new(leaf.id()).expect("leaf id is valid");
771        let md = metadata.clone().with("test", format!("rel::{}", leaf.id()));
772        let off = |reason: Reason, note: String| MatrixRow {
773            subject_kind: "capability".to_string(),
774            subject: id.clone(),
775            backend: target.backend().to_string(),
776            version: target.version().to_string(),
777            config: config.clone(),
778            enabled: false,
779            reason: Some(reason),
780            metadata: md.clone(),
781            note: note.replace(['\t', '\n'], " "),
782            extra: BTreeMap::new(),
783        };
784        let row = match (
785            &result,
786            controls.positive_passed && controls.known_bad_failed,
787        ) {
788            (Ok(()), true) => Verdict::Exact
789                .into_row(
790                    id.clone(),
791                    target.backend(),
792                    target.version(),
793                    config.clone(),
794                    md.clone(),
795                    controls,
796                )
797                .expect("both controls hold"),
798            (Ok(()), false) => off(
799                Reason::Untested,
800                format!("controls did not hold: {controls:?}"),
801            ),
802            (Err(Failure::Wrong(m)), _) => off(Reason::Wrong, m.clone()),
803            (Err(Failure::Backend(e)), _) => off(Reason::Missing, e.to_string()),
804        };
805        out.push(Certified { leaf, row });
806    }
807    out
808}