Skip to main content

cratefield_testing/
batch.rs

1//! The all-or-nothing `Database::batch_atomic` contract (issue #126),
2//! asserted against a live adapter rather than trusted from its docs. A
3//! batch that fails part-way must leave nothing behind; a batch that
4//! succeeds must commit every statement.
5
6use cratefield_core::{Database, Statement};
7
8/// Proves `db`'s `batch_atomic` is all-or-nothing: a committed batch is
9/// fully visible, a failing batch is invisible. Runs its own probe table,
10/// so it can be called against any adapter with a writable connection.
11///
12/// An adapter that cannot honour the contract fails loudly here — the
13/// port's answer to "atomic where supported" (issue #126).
14///
15/// # Panics
16///
17/// Panics when the batch contract is violated: a valid batch fails, a
18/// failed batch leaves rows behind, or the probe table cannot be created
19/// on the given connection.
20pub async fn assert_batch_is_atomic(db: &dyn Database) {
21    db.execute(&Statement::new(
22        "CREATE TABLE IF NOT EXISTS batch_atomicity_probe \
23         (id TEXT PRIMARY KEY, marker TEXT NOT NULL)",
24    ))
25    .await
26    .expect("probe table");
27
28    db.execute(&Statement::new("DELETE FROM batch_atomicity_probe"))
29        .await
30        .expect("clear probe");
31
32    let commits = vec![
33        Statement::with_values(
34            "INSERT INTO batch_atomicity_probe (id, marker) VALUES (?, ?)",
35            vec!["committed-1".into(), "batch".into()],
36        ),
37        Statement::with_values(
38            "INSERT INTO batch_atomicity_probe (id, marker) VALUES (?, ?)",
39            vec!["committed-2".into(), "batch".into()],
40        ),
41    ];
42    db.batch_atomic(&commits)
43        .await
44        .expect("a valid batch commits");
45    let rows = db
46        .query(&Statement::new(
47            "SELECT id FROM batch_atomicity_probe ORDER BY id",
48        ))
49        .await
50        .expect("read back");
51    assert_eq!(
52        rows.len(),
53        2,
54        "a committed batch leaves every statement visible"
55    );
56
57    let mid_batch = vec![
58        Statement::with_values(
59            "INSERT INTO batch_atomicity_probe (id, marker) VALUES (?, ?)",
60            vec!["doomed".into(), "must not survive".into()],
61        ),
62        Statement::new("THIS IS NOT SQL"),
63    ];
64    assert!(
65        db.batch_atomic(&mid_batch).await.is_err(),
66        "a batch with a failing statement must fail"
67    );
68    let survivors = db
69        .query(&Statement::with_values(
70            "SELECT id FROM batch_atomicity_probe WHERE id = ?",
71            vec!["doomed".into()],
72        ))
73        .await
74        .expect("read back");
75    assert!(
76        survivors.is_empty(),
77        "nothing from a failed batch is visible afterwards"
78    );
79
80    db.execute(&Statement::new("DELETE FROM batch_atomicity_probe"))
81        .await
82        .expect("clean probe");
83}