Skip to main content

macrame/integrity/
audit.rs

1use crate::error::{DbError, Result};
2
3/// Audit current belief in links_current against latest-belief projection of links (§5.8).
4/// Returns Ok(0) if zero drift, or Err(DbError::CurrentDrift { n }) if drift detected.
5///
6/// Drift is the *symmetric* difference: rows `links_current` has that the
7/// projection does not (stale or spurious materialisation) plus rows the
8/// projection has that `links_current` does not (missed materialisation).
9/// Both directions must be counted, or the audit certifies a corrupt table.
10///
11/// The two directions are computed as two independent scalar subqueries and
12/// added, rather than as one compound `SELECT`. SQLite gives `EXCEPT` and
13/// `UNION` equal precedence and evaluates them left-associatively, so writing
14/// the four arms as a flat chain parses as `(((A EXCEPT B) UNION B) EXCEPT A)`,
15/// which reduces to `A EXCEPT A` — a constant zero that reports "no drift" no
16/// matter how corrupt the table is. That was the pre-0.5.4 defect.
17///
18/// Parentheses cannot fix it: SQLite rejects a parenthesised compound-`SELECT`
19/// operand outright (`near "UNION": syntax error`). Putting each `EXCEPT` alone
20/// inside its own scalar subquery makes the grouping structural instead of
21/// syntactic, so there is no precedence for a future edit to get wrong.
22pub async fn audit_current(conn: &libsql::Connection) -> Result<usize> {
23    // `projection` is the latest-belief view of `links`: one row per interval
24    // key, the one with the greatest recorded_at. `materialized` is what
25    // links_current actually holds. Doctrine VI says they must be equal.
26    let query = format!(
27        r#"
28        WITH materialized AS (
29            SELECT source_id, target_id, edge_type, valid_from,
30                   valid_to, weight, properties, recorded_at
31            FROM links_current
32        ),
33        projection AS ({projection})
34        SELECT
35            (SELECT COUNT(*) FROM (
36                SELECT * FROM materialized EXCEPT SELECT * FROM projection))
37          + (SELECT COUNT(*) FROM (
38                SELECT * FROM projection EXCEPT SELECT * FROM materialized))
39    "#,
40        projection = super::LATEST_BELIEF_PROJECTION
41    );
42    let mut rows = conn.query(&query, ()).await?;
43    let count: i64 = if let Some(row) = rows.next().await? {
44        row.get(0).unwrap_or(0)
45    } else {
46        0
47    };
48
49    if count > 0 {
50        return Err(DbError::CurrentDrift { n: count as usize });
51    }
52    Ok(0)
53}