1use super::{
3 DATABASE,
4 receipts::{Error, check_files, schema_initialized},
5};
6use crate::{
7 repository::Identity,
8 types::{ExecutionId, RequestId, RequestStatus, RunStatus, Sha256Bytes},
9 workspace::{canonical_target, outside_workspaces},
10};
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
14pub enum Signoff {
15 Execution(ExecutionId),
16 Request(RequestId),
17}
18
19#[derive(Debug, Clone)]
20pub struct CatalogRun {
21 pub repo_path: PathBuf,
22 pub repository: Identity,
23 pub status: RunStatus,
24 pub red: u64,
25 pub error: u64,
26 pub waiting: Vec<Signoff>,
27}
28
29pub async fn read_catalog(state: &Path) -> Result<Vec<CatalogRun>, String> {
31 let state = canonical_target(state).map_err(|error| error.to_string())?;
32 check_files(&state)?;
33 if !state
34 .join(DATABASE)
35 .try_exists()
36 .map_err(|error| error.to_string())?
37 {
38 return Ok(Vec::new());
39 }
40 let connection = tokio_rusqlite::Connection::open_with_flags(
41 state.join(DATABASE),
42 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
43 )
44 .await
45 .map_err(|error| error.to_string())?;
46 let result = connection
47 .call(move |db| -> Result<_, Error> {
48 db.busy_timeout(super::SQLITE_BUSY_TIMEOUT)?;
49 let transaction = db.transaction()?;
50 if !schema_initialized(&transaction)? {
51 return Ok(Vec::new());
52 }
53 let catalog = in_snapshot(&transaction, &state)?;
54 transaction.commit()?;
55 Ok(catalog)
56 })
57 .await
58 .map_err(|error| error.to_string());
59 super::connection::finish(connection, result).await
60}
61
62pub(super) fn in_snapshot(
63 db: &rusqlite::Connection,
64 state: &Path,
65) -> Result<Vec<CatalogRun>, Error> {
66 in_snapshot_cached(db, state, &mut Facts::default())
67}
68
69#[derive(Default)]
70pub(super) struct Facts {
71 runs: std::collections::BTreeMap<crate::types::RunId, (Sha256Bytes, Option<CatalogRun>)>,
72}
73
74pub(super) fn in_snapshot_cached(
75 db: &rusqlite::Connection,
76 state: &Path,
77 facts: &mut Facts,
78) -> Result<Vec<CatalogRun>, Error> {
79 let mut statement = db.prepare_cached("SELECT id,data FROM runs ORDER BY rowid")?;
80 let runs = statement
81 .query_map([], |row| {
82 Ok((
83 row.get::<_, crate::types::RunId>(0)?,
84 row.get::<_, String>(1)?,
85 ))
86 })?
87 .collect::<Result<Vec<_>, _>>()?;
88 let mut catalog = std::collections::BTreeMap::new();
89 let mut order = Vec::new();
90 let mut retained = std::collections::BTreeMap::new();
91 for (id, data) in runs {
92 let digest = Sha256Bytes::of(data.as_bytes());
93 let fact = match facts.runs.remove(&id) {
94 Some((saved, fact)) if saved == digest => fact,
95 _ => super::unreadable::evidence::<super::Run>("run", &data).map(|run| {
96 let status = run.status();
97 CatalogRun {
98 repo_path: run.repo_path,
99 repository: run.repository,
100 status,
101 red: 0,
102 error: 0,
103 waiting: Vec::new(),
104 }
105 }),
106 };
107 retained.insert(id.clone(), (digest, fact.clone()));
108 if let Some(fact) = fact {
109 order.push(id.clone());
110 catalog.insert(id, fact);
111 }
112 }
113 outside_workspaces(catalog.values().map(|run| run.repo_path.as_path()), state)
114 .map_err(|error| Error::Invalid(error.to_string()))?;
115 facts.runs = retained;
116 let mut statement = db.prepare_cached(
117 "SELECT q.run_id,q.status,q.execution_id,coalesce(json_extract(e.data,'$.provenance.requestId'),json_extract(q.data,'$.provenance.requestId'),q.id)
118 FROM requests q LEFT JOIN executions e ON e.id=q.execution_id WHERE q.status IN ('RED','ERROR','WAITING_HUMAN','DEFERRED') ORDER BY q.run_id,q.ordinal",
119 )?;
120 let attention = statement.query_map([], |row| {
121 Ok((
122 row.get::<_, crate::types::RunId>(0)?,
123 row.get::<_, RequestStatus>(1)?,
124 row.get::<_, Option<ExecutionId>>(2)?,
125 row.get::<_, RequestId>(3)?,
126 ))
127 })?;
128 for row in attention {
129 let (id, status, execution, request) = row?;
130 if let Some(run) = catalog.get_mut(&id) {
131 match status {
132 RequestStatus::Red => run.red += 1,
133 RequestStatus::Error => run.error += 1,
134 RequestStatus::WaitingHuman | RequestStatus::Deferred => run
136 .waiting
137 .push(execution.map_or(Signoff::Request(request), Signoff::Execution)),
138 _ => unreachable!("attention query filters states"),
139 }
140 }
141 }
142 Ok(order
143 .into_iter()
144 .filter_map(|id| catalog.remove(&id))
145 .collect())
146}