1use crate::{Dialect, RelError, RelationalBackend, RelationalTx, Row, Value};
26use persistant_core::{CapabilityId, Config, Controls, Reason, Row as MatrixRow, Verdict};
27use std::collections::BTreeMap;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum Leaf {
33 Values,
35 Transaction,
37 UniqueClassified,
39 ForeignKey,
41 CheckConstraint,
43}
44
45impl Leaf {
46 pub const ALL: [Leaf; 5] = [
48 Leaf::Values,
49 Leaf::Transaction,
50 Leaf::UniqueClassified,
51 Leaf::ForeignKey,
52 Leaf::CheckConstraint,
53 ];
54
55 pub fn id(self) -> &'static str {
57 match self {
58 Leaf::Values => "rel.values",
59 Leaf::Transaction => "rel.transaction",
60 Leaf::UniqueClassified => "rel.error.unique",
61 Leaf::ForeignKey => "rel.foreign_key",
62 Leaf::CheckConstraint => "rel.check_constraint",
63 }
64 }
65
66 fn defects(self) -> &'static [Defect] {
67 match self {
68 Leaf::Values => &[Defect::LossyBytes],
69 Leaf::Transaction => &[Defect::NoTransactions],
70 Leaf::UniqueClassified => &[Defect::UnclassifiedErrors],
71 Leaf::ForeignKey | Leaf::CheckConstraint => &[],
72 }
73 }
74
75 fn has_unconstrained_control(self) -> bool {
77 matches!(self, Leaf::ForeignKey | Leaf::CheckConstraint)
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83#[non_exhaustive]
84pub enum Defect {
85 NoTransactions,
88 LossyBytes,
90 UnclassifiedErrors,
92}
93
94#[derive(Debug)]
96pub struct Defective<'a, R> {
97 inner: &'a R,
98 defect: Defect,
99}
100
101impl<'a, R: RelationalBackend> Defective<'a, R> {
102 pub fn new(inner: &'a R, defect: Defect) -> Self {
104 Defective { inner, defect }
105 }
106}
107
108pub enum DefectiveTx<'a, R: RelationalBackend + 'a> {
110 Real(R::Tx<'a>, Defect),
112 Direct(&'a R),
114}
115
116impl<R: RelationalBackend> RelationalBackend for Defective<'_, R> {
117 type Tx<'a>
118 = DefectiveTx<'a, R>
119 where
120 Self: 'a;
121
122 fn backend(&self) -> &str {
123 self.inner.backend()
124 }
125
126 fn version(&self) -> &str {
127 self.inner.version()
128 }
129
130 fn dialect(&self) -> Dialect {
131 self.inner.dialect()
132 }
133
134 async fn execute(&self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
135 self.inner
136 .execute(sql, &lossy(self.defect, params))
137 .await
138 .map_err(|e| unclassify(self.defect, e))
139 }
140
141 async fn query(&self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
142 self.inner
143 .query(sql, &lossy(self.defect, params))
144 .await
145 .map_err(|e| unclassify(self.defect, e))
146 }
147
148 async fn begin(&self) -> Result<DefectiveTx<'_, R>, RelError> {
149 if self.defect == Defect::NoTransactions {
150 return Ok(DefectiveTx::Direct(self.inner));
151 }
152 Ok(DefectiveTx::Real(
153 self.inner
154 .begin()
155 .await
156 .map_err(|e| unclassify(self.defect, e))?,
157 self.defect,
158 ))
159 }
160}
161
162fn lossy(defect: Defect, params: &[Value]) -> Vec<Value> {
163 params
164 .iter()
165 .map(|p| match (defect, p) {
166 (Defect::LossyBytes, Value::Bytes(b)) => {
167 Value::Bytes(b.iter().take(1).copied().collect())
168 }
169 _ => p.clone(),
170 })
171 .collect()
172}
173
174fn unclassify(defect: Defect, e: RelError) -> RelError {
175 if defect == Defect::UnclassifiedErrors {
176 RelError::Backend(e.to_string())
177 } else {
178 e
179 }
180}
181
182impl<R: RelationalBackend> RelationalTx for DefectiveTx<'_, R> {
183 async fn execute(&mut self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
184 match self {
185 DefectiveTx::Real(tx, d) => tx
186 .execute(sql, &lossy(*d, params))
187 .await
188 .map_err(|e| unclassify(*d, e)),
189 DefectiveTx::Direct(db) => db.execute(sql, params).await,
190 }
191 }
192
193 async fn query(&mut self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
194 match self {
195 DefectiveTx::Real(tx, d) => tx
196 .query(sql, &lossy(*d, params))
197 .await
198 .map_err(|e| unclassify(*d, e)),
199 DefectiveTx::Direct(db) => db.query(sql, params).await,
200 }
201 }
202
203 async fn commit(self) -> Result<(), RelError> {
204 match self {
205 DefectiveTx::Real(tx, d) => tx.commit().await.map_err(|e| unclassify(d, e)),
206 DefectiveTx::Direct(_) => Ok(()),
207 }
208 }
209
210 async fn rollback(self) -> Result<(), RelError> {
211 match self {
212 DefectiveTx::Real(tx, d) => tx.rollback().await.map_err(|e| unclassify(d, e)),
213 DefectiveTx::Direct(_) => Ok(()),
214 }
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
220pub enum Failure {
221 Wrong(String),
223 Backend(RelError),
225}
226
227impl From<RelError> for Failure {
228 fn from(e: RelError) -> Self {
229 Failure::Backend(e)
230 }
231}
232
233fn wrong<T>(msg: impl Into<String>) -> Result<T, Failure> {
234 Err(Failure::Wrong(msg.into()))
235}
236
237#[derive(Clone, Copy)]
240enum Ty {
241 Int,
242 Text,
243 Bytes,
244 Float,
245}
246
247struct Sql(Dialect);
248
249impl Sql {
250 fn p(&self, n: usize) -> String {
252 match self.0 {
253 Dialect::Postgres => format!("${n}"),
254 Dialect::Sqlite => format!("?{n}"),
255 _ => "?".to_string(),
256 }
257 }
258
259 fn ty(&self, t: Ty) -> &'static str {
260 match (&self.0, t) {
261 (_, Ty::Int) => "BIGINT",
262 (Dialect::Postgres, Ty::Text) => "TEXT",
263 (Dialect::Sqlite, Ty::Text) => "TEXT",
264 (_, Ty::Text) => "VARCHAR(255)",
265 (Dialect::Postgres, Ty::Bytes) => "BYTEA",
266 (Dialect::Sqlite, Ty::Bytes) => "BLOB",
267 (_, Ty::Bytes) => "LONGBLOB",
268 (Dialect::Postgres, Ty::Float) => "DOUBLE PRECISION",
269 (_, Ty::Float) => "DOUBLE",
270 }
271 }
272
273 fn tail(&self) -> &'static str {
275 match self.0 {
276 Dialect::MySql | Dialect::MariaDb => " ENGINE=InnoDB",
277 _ => "",
278 }
279 }
280
281 fn pk(&self, name: &str) -> String {
283 format!("{name} BIGINT NOT NULL PRIMARY KEY")
284 }
285}
286
287fn supported(d: &Dialect) -> Result<(), Failure> {
288 match d {
289 Dialect::Postgres | Dialect::Sqlite | Dialect::MySql | Dialect::MariaDb => Ok(()),
290 other => Err(Failure::Backend(RelError::Unsupported(format!(
291 "the relational contract tests have no statements for {other}"
292 )))),
293 }
294}
295
296async fn drop_tables<R: RelationalBackend>(b: &R, names: &[&str]) -> Result<(), RelError> {
297 for n in names {
298 b.execute(&format!("DROP TABLE IF EXISTS {n}"), &[]).await?;
299 }
300 Ok(())
301}
302
303pub async fn run_leaf<R: RelationalBackend>(
307 leaf: Leaf,
308 b: &R,
309 constrained: bool,
310) -> Result<(), Failure> {
311 let d = b.dialect();
312 supported(&d)?;
313 let s = Sql(d);
314 match leaf {
315 Leaf::Values => values(b, &s).await,
316 Leaf::Transaction => transaction(b, &s).await,
317 Leaf::UniqueClassified => unique(b, &s).await,
318 Leaf::ForeignKey => foreign_key(b, &s, constrained).await,
319 Leaf::CheckConstraint => check(b, &s, constrained).await,
320 }
321}
322
323async fn values<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
324 const T: &str = "persistant_rel_values";
325 drop_tables(b, &[T]).await?;
326 b.execute(
327 &format!(
328 "CREATE TABLE {T} ({}, i {}, t {}, b {}, f {}){}",
329 s.pk("id"),
330 s.ty(Ty::Int),
331 s.ty(Ty::Text),
332 s.ty(Ty::Bytes),
333 s.ty(Ty::Float),
334 s.tail()
335 ),
336 &[],
337 )
338 .await?;
339 let cases: Vec<(i64, Value, Value, Value, Value)> = vec![
340 (
341 1,
342 Value::Int(i64::MIN),
343 Value::Text("ünï ✓ \u{1F980}".to_string()),
344 Value::Bytes(vec![0x00, 0xFF, 0x80, 0x00, 0x7F]),
345 Value::Float(1.5),
346 ),
347 (
348 2,
349 Value::Int(i64::MAX),
350 Value::Text(String::new()),
351 Value::Bytes(vec![]),
352 Value::Float(-0.25),
353 ),
354 (3, Value::Null, Value::Null, Value::Null, Value::Null),
355 ];
356 for (id, i, t, bytes, f) in &cases {
357 b.execute(
358 &format!(
359 "INSERT INTO {T} (id, i, t, b, f) VALUES ({}, {}, {}, {}, {})",
360 s.p(1),
361 s.p(2),
362 s.p(3),
363 s.p(4),
364 s.p(5)
365 ),
366 &[
367 Value::Int(*id),
368 i.clone(),
369 t.clone(),
370 bytes.clone(),
371 f.clone(),
372 ],
373 )
374 .await?;
375 }
376 let rows = b
377 .query(&format!("SELECT id, i, t, b, f FROM {T} ORDER BY id"), &[])
378 .await?;
379 drop_tables(b, &[T]).await?;
380 if rows.len() != cases.len() {
381 return wrong(format!("wrote {} rows, read {}", cases.len(), rows.len()));
382 }
383 for (row, (id, i, t, bytes, f)) in rows.iter().zip(&cases) {
384 let want = [
385 Value::Int(*id),
386 i.clone(),
387 t.clone(),
388 bytes.clone(),
389 f.clone(),
390 ];
391 if row.values != want {
393 return wrong(format!("row {id}: wrote {want:?}, read {:?}", row.values));
394 }
395 }
396 Ok(())
397}
398
399async fn count<R: RelationalBackend>(b: &R, table: &str) -> Result<i64, Failure> {
400 let r = b
401 .query(&format!("SELECT COUNT(*) FROM {table}"), &[])
402 .await?;
403 match r.first().and_then(|r| r.int(0)) {
404 Some(n) => Ok(n),
405 None => wrong("COUNT(*) did not come back as an integer"),
406 }
407}
408
409async fn transaction<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
410 const T: &str = "persistant_rel_tx";
411 drop_tables(b, &[T]).await?;
412 b.execute(
413 &format!(
414 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
415 s.pk("id"),
416 s.tail()
417 ),
418 &[],
419 )
420 .await?;
421 let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
422 let result = async {
423 let mut tx = b.begin().await?;
425 tx.execute(&ins, &[Value::Int(1), Value::Int(10)]).await?;
426 let own = tx.query(&format!("SELECT COUNT(*) FROM {T}"), &[]).await?;
427 if own.first().and_then(|r| r.int(0)) != Some(1) {
428 return wrong("a transaction cannot see its own write");
429 }
430 tx.commit().await?;
431 if count(b, T).await? != 1 {
432 return wrong("a committed write is not visible");
433 }
434 let mut tx = b.begin().await?;
436 tx.execute(&ins, &[Value::Int(2), Value::Int(20)]).await?;
437 tx.rollback().await?;
438 if count(b, T).await? != 1 {
439 return wrong("a rolled-back write is visible");
440 }
441 {
443 let mut tx = b.begin().await?;
444 tx.execute(&ins, &[Value::Int(3), Value::Int(30)]).await?;
445 }
446 if count(b, T).await? != 1 {
447 return wrong("a transaction dropped without commit was not rolled back");
448 }
449 Ok(())
450 }
451 .await;
452 drop_tables(b, &[T]).await?;
453 result
454}
455
456async fn unique<R: RelationalBackend>(b: &R, s: &Sql) -> Result<(), Failure> {
457 const T: &str = "persistant_rel_unique";
458 drop_tables(b, &[T]).await?;
459 b.execute(
460 &format!(
461 "CREATE TABLE {T} ({}, n BIGINT NOT NULL){}",
462 s.pk("id"),
463 s.tail()
464 ),
465 &[],
466 )
467 .await?;
468 let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
469 let result = async {
470 b.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
471 match b.execute(&ins, &[Value::Int(1), Value::Int(2)]).await {
472 Err(RelError::UniqueViolation { .. }) => {}
473 other => return wrong(format!("a duplicate key came back as {other:?}")),
474 }
475 let mut tx = b.begin().await?;
477 let dup = tx.execute(&ins, &[Value::Int(1), Value::Int(3)]).await;
478 let _ = tx.rollback().await;
479 match dup {
480 Err(RelError::UniqueViolation { .. }) => Ok(()),
481 other => wrong(format!(
482 "a duplicate key in a transaction came back as {other:?}"
483 )),
484 }
485 }
486 .await;
487 drop_tables(b, &[T]).await?;
488 result
489}
490
491async fn foreign_key<R: RelationalBackend>(
492 b: &R,
493 s: &Sql,
494 constrained: bool,
495) -> Result<(), Failure> {
496 const P: &str = "persistant_rel_fk_parent";
497 const C: &str = "persistant_rel_fk_child";
498 drop_tables(b, &[C, P]).await?;
499 b.execute(
500 &format!("CREATE TABLE {P} ({}){}", s.pk("id"), s.tail()),
501 &[],
502 )
503 .await?;
504 let refs = if constrained {
505 format!(", FOREIGN KEY (pid) REFERENCES {P}(id)")
506 } else {
507 String::new()
508 };
509 b.execute(
510 &format!(
511 "CREATE TABLE {C} ({}, pid BIGINT NOT NULL{refs}){}",
512 s.pk("id"),
513 s.tail()
514 ),
515 &[],
516 )
517 .await?;
518 let result = async {
519 b.execute(
520 &format!("INSERT INTO {P} (id) VALUES ({})", s.p(1)),
521 &[Value::Int(1)],
522 )
523 .await?;
524 let ins = format!("INSERT INTO {C} (id, pid) VALUES ({}, {})", s.p(1), s.p(2));
525 b.execute(&ins, &[Value::Int(1), Value::Int(1)]).await?;
526 match b.execute(&ins, &[Value::Int(2), Value::Int(99)]).await {
527 Err(RelError::ForeignKeyViolation { .. }) => {}
528 Ok(_) => return wrong("a reference to a missing row was accepted"),
529 other => {
530 return wrong(format!(
531 "a reference to a missing row came back as {other:?}"
532 ));
533 }
534 }
535 match b
536 .execute(
537 &format!("DELETE FROM {P} WHERE id = {}", s.p(1)),
538 &[Value::Int(1)],
539 )
540 .await
541 {
542 Err(RelError::ForeignKeyViolation { .. }) => Ok(()),
543 Ok(_) => wrong("a referenced row was deleted"),
544 other => wrong(format!("deleting a referenced row came back as {other:?}")),
545 }
546 }
547 .await;
548 drop_tables(b, &[C, P]).await?;
549 result
550}
551
552async fn check<R: RelationalBackend>(b: &R, s: &Sql, constrained: bool) -> Result<(), Failure> {
553 const T: &str = "persistant_rel_check";
554 drop_tables(b, &[T]).await?;
555 let ck = if constrained { " CHECK (n >= 0)" } else { "" };
556 b.execute(
557 &format!(
558 "CREATE TABLE {T} ({}, n BIGINT NOT NULL{ck}){}",
559 s.pk("id"),
560 s.tail()
561 ),
562 &[],
563 )
564 .await?;
565 let ins = format!("INSERT INTO {T} (id, n) VALUES ({}, {})", s.p(1), s.p(2));
566 let result = async {
567 b.execute(&ins, &[Value::Int(1), Value::Int(0)]).await?;
568 match b.execute(&ins, &[Value::Int(2), Value::Int(-1)]).await {
569 Err(RelError::CheckViolation { .. }) => Ok(()),
570 Ok(_) => wrong("a row violating a CHECK was accepted"),
571 other => wrong(format!("a row violating a CHECK came back as {other:?}")),
572 }
573 }
574 .await;
575 drop_tables(b, &[T]).await?;
576 result
577}
578
579#[derive(Debug, Clone, PartialEq, Eq)]
583pub struct Certified {
584 pub leaf: Leaf,
586 pub row: MatrixRow,
588}
589
590pub async fn certify<R: RelationalBackend>(
596 target: &R,
597 config: &Config,
598 metadata: &Config,
599) -> Vec<Certified> {
600 let mut out = Vec::new();
601 for leaf in Leaf::ALL {
602 let result = run_leaf(leaf, target, true).await;
603 let positive_passed = !matches!(
604 &result,
605 Err(Failure::Backend(
606 RelError::Unsupported(_) | RelError::Unavailable(_)
607 ))
608 );
609 let mut known_bad_failed = true;
610 for d in leaf.defects() {
611 let bad = Defective::new(target, *d);
612 if run_leaf(leaf, &bad, true).await.is_ok() {
613 known_bad_failed = false;
614 }
615 }
616 if leaf.has_unconstrained_control() && run_leaf(leaf, target, false).await.is_ok() {
617 known_bad_failed = false;
618 }
619 let controls = Controls {
620 positive_passed,
621 known_bad_failed,
622 };
623 let id = CapabilityId::new(leaf.id()).expect("leaf id is valid");
624 let md = metadata.clone().with("test", format!("rel::{}", leaf.id()));
625 let off = |reason: Reason, note: String| MatrixRow {
626 subject_kind: "capability".to_string(),
627 subject: id.clone(),
628 backend: target.backend().to_string(),
629 version: target.version().to_string(),
630 config: config.clone(),
631 enabled: false,
632 reason: Some(reason),
633 metadata: md.clone(),
634 note: note.replace(['\t', '\n'], " "),
635 extra: BTreeMap::new(),
636 };
637 let row = match (
638 &result,
639 controls.positive_passed && controls.known_bad_failed,
640 ) {
641 (Ok(()), true) => Verdict::Exact
642 .into_row(
643 id.clone(),
644 target.backend(),
645 target.version(),
646 config.clone(),
647 md.clone(),
648 controls,
649 )
650 .expect("both controls hold"),
651 (Ok(()), false) => off(
652 Reason::Untested,
653 format!("controls did not hold: {controls:?}"),
654 ),
655 (Err(Failure::Wrong(m)), _) => off(Reason::Wrong, m.clone()),
656 (Err(Failure::Backend(e)), _) => off(Reason::Missing, e.to_string()),
657 };
658 out.push(Certified { leaf, row });
659 }
660 out
661}