Skip to main content

artifactize_core/store/
unreadable.rs

1//! Per-record corruption reporting, separate from valid lifecycle models.
2
3use std::{collections::BTreeSet, path::Path};
4
5use rusqlite::params;
6use serde::{Deserialize, Serialize, de::DeserializeOwned};
7
8use super::receipts::Error;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct Unreadable {
13    pub kind: String,
14    pub id: String,
15    pub reason: String,
16}
17
18impl std::fmt::Display for Unreadable {
19    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        write!(out, "Unreadable {} {}: {}", self.kind, self.id, self.reason)
21    }
22}
23
24pub(super) fn decode<T: DeserializeOwned>(
25    kind: &'static str,
26    id: &str,
27    data: &str,
28) -> Result<T, Unreadable> {
29    serde_json::from_str(data).map_err(|error| Unreadable {
30        kind: kind.into(),
31        id: id.to_owned(),
32        reason: error.to_string(),
33    })
34}
35
36/// Reads used only for evidence skip one bad record and explain which record was skipped.
37pub(super) fn evidence<T: DeserializeOwned>(kind: &'static str, data: &str) -> Option<T> {
38    match serde_json::from_str(data) {
39        Ok(record) => Some(record),
40        Err(error) => {
41            // Decode the identity only on failure, not the whole payload a second time.
42            #[derive(Deserialize)]
43            struct Identity {
44                id: String,
45            }
46            let id = serde_json::from_str::<Identity>(data)
47                .map(|identity| identity.id)
48                .unwrap_or_else(|_| "(unknown id)".into());
49            skipped(&Unreadable {
50                kind: kind.into(),
51                id,
52                reason: error.to_string(),
53            });
54            None
55        }
56    }
57}
58
59/// Explain a record a read skipped.
60pub(super) fn skipped(unreadable: &Unreadable) {
61    use std::io::Write;
62    let _ = writeln!(std::io::stderr().lock(), "{unreadable}");
63}
64
65/// Existing-state audit for current-input status and the monitor, without rejecting a listing.
66pub async fn read_unreadable(state: &Path, repo: Option<&Path>) -> Result<Vec<Unreadable>, String> {
67    super::receipts::check_files(state)?;
68    if !state
69        .join(super::DATABASE)
70        .try_exists()
71        .map_err(|error| error.to_string())?
72    {
73        return Ok(Vec::new());
74    }
75    let connection = tokio_rusqlite::Connection::open_with_flags(
76        state.join(super::DATABASE),
77        rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
78    )
79    .await
80    .map_err(|error| error.to_string())?;
81    let repo = repo.map(crate::platform::path_text);
82    let result = connection
83        .call(move |db| -> Result<_, Error> {
84            super::wait_when_busy(db)?;
85            let transaction = db.transaction()?;
86            if !super::receipts::schema_initialized(&transaction)? {
87                return Ok(Vec::new());
88            }
89            let mut statement =
90                transaction.prepare("SELECT id FROM runs WHERE ?1 IS NULL OR repo=?1")?;
91            let runs = statement
92                .query_map(params![repo], |row| row.get::<_, crate::types::RunId>(0))?
93                .collect::<Result<Vec<_>, _>>()?;
94            let mut unreadable = Vec::new();
95            let mut seen = BTreeSet::new();
96            for error in in_runs(&transaction, &runs)?.into_values().flatten() {
97                if seen.insert((error.kind.clone(), error.id.clone())) {
98                    unreadable.push(error);
99                }
100            }
101            Ok(unreadable)
102        })
103        .await
104        .map_err(|error| error.to_string());
105    super::connection::finish(connection, result).await
106}
107
108/// Audit the given Runs in one query: their state and definitions rows, their requests and
109/// executions, including executions produced by them but no longer linked. Every record is
110/// decoded once; nothing is kept between audits.
111pub(super) fn in_runs(
112    db: &rusqlite::Connection,
113    runs: &[crate::types::RunId],
114) -> Result<std::collections::BTreeMap<crate::types::RunId, Vec<Unreadable>>, Error> {
115    let mut statement = db.prepare_cached(
116        "WITH page(id) AS (SELECT value FROM json_each(?1))
117        SELECT r.id,'run',r.id,r.data FROM runs r JOIN page p ON p.id=r.id
118        UNION ALL SELECT d.run_id,'definitions',d.run_id,d.data FROM run_definitions d JOIN page p ON p.id=d.run_id
119        UNION ALL SELECT q.run_id,'request',q.id,q.data FROM requests q JOIN page p ON p.id=q.run_id
120        UNION ALL SELECT q.run_id,'execution',e.id,e.data FROM (SELECT DISTINCT run_id,execution_id FROM requests WHERE run_id IN (SELECT id FROM page) AND execution_id IS NOT NULL) q JOIN executions e ON e.id=q.execution_id
121        UNION ALL SELECT json_extract(e.data,'$.provenance.runId'),'execution',e.id,e.data FROM executions e WHERE json_extract(e.data,'$.provenance.runId') IN (SELECT id FROM page)",
122    )?;
123    let records = statement.query_map([serde_json::to_string(runs)?], |row| {
124        Ok((
125            row.get::<_, crate::types::RunId>(0)?,
126            row.get::<_, String>(1)?,
127            row.get::<_, String>(2)?,
128            row.get::<_, String>(3)?,
129        ))
130    })?;
131    let mut unreadable: std::collections::BTreeMap<_, Vec<_>> = std::collections::BTreeMap::new();
132    let mut seen = BTreeSet::new();
133    for record in records {
134        let (run, kind, id, data) = record?;
135        if !seen.insert((run.clone(), kind.clone(), id.clone())) {
136            continue;
137        }
138        let error = match kind.as_str() {
139            "run" => decode::<super::Run>("run", &id, &data).err(),
140            // A Run whose definitions do not read is an unreadable Run.
141            "definitions" => decode::<super::definitions::Definitions>("run", &id, &data).err(),
142            "request" => decode::<super::Request>("request", &id, &data).err(),
143            _ => decode::<super::Execution>("execution", &id, &data).err(),
144        };
145        if let Some(error) = error {
146            unreadable.entry(run).or_default().push(error);
147        }
148    }
149    for records in unreadable.values_mut() {
150        records.sort_by(|a, b| (&a.kind, &a.id).cmp(&(&b.kind, &b.id)));
151    }
152    Ok(unreadable)
153}