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}