1use std::path::Path;
2
3use serde::Serialize;
4
5use tokio_rusqlite::Connection;
6
7use super::{
8 DATABASE, Execution, HumanClaim, Request,
9 receipts::{Error, check_files, schema_initialized},
10};
11use crate::workspace::{canonical_target, outside_workspace};
12
13#[derive(Debug, Clone, Serialize)]
14pub struct RequestView {
15 #[serde(flatten)]
16 pub request: Request,
17 pub claim: Option<HumanClaim>,
18 pub execution: Option<Execution>,
19 pub definition: Option<RequestDefinition>,
20}
21
22#[derive(Debug, Clone, Serialize)]
23pub struct RequestDefinition {
24 pub eval: super::definitions::Eval,
25 pub artifact: super::definitions::Artifact,
26}
27
28#[derive(Default)]
29struct Filter<'a> {
30 run: Option<&'a str>,
31 id: Option<&'a str>,
32 repo: Option<&'a Path>,
33 waiting: bool,
34 session: Option<&'a str>,
35 statuses: &'a [crate::types::RequestStatus],
36 slice: Option<(u64, u32)>,
38}
39
40pub async fn read_requests(state: &Path, run: Option<&str>) -> Result<Vec<RequestView>, String> {
41 read(
42 state,
43 Filter {
44 run,
45 ..Filter::default()
46 },
47 )
48 .await
49}
50
51pub async fn read_request(state: &Path, id: &str) -> Result<RequestView, String> {
52 read(
53 state,
54 Filter {
55 id: Some(id),
56 ..Filter::default()
57 },
58 )
59 .await?
60 .pop()
61 .ok_or_else(|| "Review request not found.".into())
62}
63
64pub async fn read_session_request(state: &Path, id: &str) -> Result<Option<RequestView>, String> {
66 Ok(read(
67 state,
68 Filter {
69 session: Some(id),
70 ..Filter::default()
71 },
72 )
73 .await?
74 .pop())
75}
76
77pub async fn read_original(state: &Path, view: &RequestView) -> Result<RequestView, String> {
81 let original = view
82 .execution
83 .as_ref()
84 .map(|execution| &execution.provenance.request_id)
85 .or_else(|| {
86 view.request
87 .provenance
88 .as_ref()
89 .map(|source| &source.request_id)
90 });
91 match original.filter(|id| *id != &view.request.id) {
92 Some(id) => read_request(state, id).await,
93 None => Ok(view.clone()),
94 }
95}
96
97pub async fn read_waiting_originals(
100 state: &Path,
101 repo: Option<&Path>,
102) -> Result<Vec<RequestView>, String> {
103 let waiting = read_waiting(state, repo).await?;
104 let by_id: std::collections::BTreeMap<_, _> = waiting
105 .iter()
106 .map(|view| (&view.request.id, view))
107 .collect();
108 let mut seen = std::collections::BTreeSet::new();
109 let mut originals: Vec<RequestView> = Vec::with_capacity(waiting.len());
110 for view in &waiting {
111 let source = view
112 .execution
113 .as_ref()
114 .map_or(&view.request.id, |execution| {
115 &execution.provenance.request_id
116 });
117 if seen.contains(source) {
118 continue;
119 }
120 let original = match by_id.get(source) {
121 Some(original) => Some((*original).clone()),
122 None => read_original(state, view).await.ok(),
123 };
124 let original = original
126 .filter(|original| {
127 original.request.status() == crate::types::RequestStatus::WaitingHuman
128 })
129 .unwrap_or_else(|| view.clone());
130 if seen.insert(original.request.id.clone()) {
131 originals.push(original);
132 }
133 }
134 Ok(originals)
135}
136
137pub async fn read_request_batch(
140 state: &Path,
141 statuses: &[crate::types::RequestStatus],
142 offset: u64,
143 limit: u32,
144) -> Result<Vec<(RequestView, std::path::PathBuf)>, String> {
145 read_with_repo(
146 state,
147 Filter {
148 statuses,
149 slice: Some((offset, limit)),
150 ..Filter::default()
151 },
152 )
153 .await
154}
155
156pub async fn read_waiting(state: &Path, repo: Option<&Path>) -> Result<Vec<RequestView>, String> {
158 read(
159 state,
160 Filter {
161 repo,
162 waiting: true,
163 ..Filter::default()
164 },
165 )
166 .await
167}
168
169async fn read(state: &Path, filter: Filter<'_>) -> Result<Vec<RequestView>, String> {
170 Ok(read_with_repo(state, filter)
171 .await?
172 .into_iter()
173 .map(|(view, _)| view)
174 .collect())
175}
176
177pub(crate) async fn read_request_repo(
179 state: &Path,
180 id: &str,
181) -> Result<(RequestView, std::path::PathBuf), String> {
182 read_with_repo(
183 state,
184 Filter {
185 id: Some(id),
186 ..Filter::default()
187 },
188 )
189 .await?
190 .pop()
191 .ok_or_else(|| "Review request not found.".into())
192}
193async fn read_with_repo(
194 state: &Path,
195 filter: Filter<'_>,
196) -> Result<Vec<(RequestView, std::path::PathBuf)>, String> {
197 let state = canonical_target(state).map_err(|e| e.to_string())?;
198 let repo = filter
199 .repo
200 .map(canonical_target)
201 .transpose()
202 .map_err(|e| e.to_string())?;
203 if let Some(repo) = &repo {
204 outside_workspace(repo, &state).map_err(|e| e.to_string())?;
205 }
206 check_files(&state)?;
207 if !state
208 .join(DATABASE)
209 .try_exists()
210 .map_err(|e| e.to_string())?
211 {
212 return Ok(Vec::new());
213 }
214 let connection = Connection::open_with_flags(
215 state.join(DATABASE),
216 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
217 )
218 .await
219 .map_err(|e| e.to_string())?;
220 let run = filter
221 .run
222 .map(str::parse::<crate::types::RunId>)
223 .transpose()?;
224 let id = filter
225 .id
226 .map(str::parse::<crate::types::RequestId>)
227 .transpose()?;
228 let repo = repo.map(|repo| crate::platform::path_text(&repo));
229 let waiting = filter.waiting;
230 let statuses = filter.statuses.to_vec();
231 let slice = filter.slice;
232 let session = filter
233 .session
234 .map(str::parse::<crate::types::SessionId>)
235 .transpose()?;
236 connection
237 .call(move |db| -> Result<_, Error> {
238 db.busy_timeout(super::SQLITE_BUSY_TIMEOUT)?;
239 let transaction = db.transaction()?;
240 if !schema_initialized(&transaction)? {
241 return Ok(Vec::new());
242 }
243 let filter = Query {
244 run,
245 id,
246 repo,
247 waiting,
248 session,
249 statuses,
250 slice,
251 };
252 let rows = rows(&transaction, &filter)?;
253 let ids: std::collections::BTreeSet<_> = rows.iter().map(|row| &row.run).collect();
254 let mut statement = transaction.prepare(
255 "SELECT id,repo,data FROM runs WHERE id IN (SELECT value FROM json_each(?))",
256 )?;
257 let runs = statement
258 .query_map([serde_json::to_string(&ids)?], |row| {
259 Ok((
260 row.get::<_, crate::types::RunId>(0)?,
261 row.get::<_, String>(1)?,
262 row.get::<_, String>(2)?,
263 ))
264 })?
265 .collect::<Result<Vec<_>, _>>()?;
266 let mut indexes = std::collections::BTreeMap::new();
267 let mut repos = Vec::new();
268 for (id, repo, data) in runs {
269 let Some(run) = super::unreadable::evidence::<super::Run>("run", &data) else {
270 continue;
271 };
272 repos.push(std::path::PathBuf::from(repo));
273 indexes.insert(id, (DefinitionIndex::new(&run), run.repo_path));
274 }
275 crate::workspace::outside_workspaces(repos.iter().map(|repo| repo.as_path()), &state)
276 .map_err(|error| Error::Invalid(error.to_string()))?;
277 let views = rows
278 .into_iter()
279 .filter_map(|row| {
280 indexes
281 .get(&row.run)
282 .and_then(|(index, repo)| row.view(index).map(|view| (view, repo.clone())))
283 })
284 .collect();
285 drop(statement);
286 transaction.commit()?;
287 Ok(views)
288 })
289 .await
290 .map_err(|e| e.to_string())
291}
292
293#[derive(Default)]
295pub(super) struct Query {
296 pub run: Option<crate::types::RunId>,
297 pub id: Option<crate::types::RequestId>,
298 pub repo: Option<String>,
299 pub waiting: bool,
300 pub session: Option<crate::types::SessionId>,
301 pub statuses: Vec<crate::types::RequestStatus>,
302 pub slice: Option<(u64, u32)>,
303}
304
305#[derive(Debug, Clone, PartialEq, Eq)]
306pub(super) struct Row {
307 pub id: crate::types::RequestId,
308 pub run: crate::types::RunId,
309 pub eval: crate::types::EvalId,
310 pub ordinal: i64,
311 pub data: String,
312 pub execution: Option<String>,
313 pub execution_id: Option<crate::types::ExecutionId>,
314 pub claim: Option<(
315 crate::types::RequestId,
316 crate::types::ReviewerId,
317 crate::types::Timestamp,
318 )>,
319}
320
321pub(super) fn rows(db: &rusqlite::Connection, filter: &Query) -> Result<Vec<Row>, Error> {
322 let mut predicates = Vec::new();
323 let mut values: Vec<rusqlite::types::Value> = Vec::new();
324 for (column, value) in [
325 ("q.run_id", filter.run.as_ref().map(ToString::to_string)),
326 ("q.id", filter.id.as_ref().map(ToString::to_string)),
327 ("r.repo", filter.repo.clone()),
328 (
329 "json_extract(q.data,'$.sessionId')",
330 filter.session.as_ref().map(ToString::to_string),
331 ),
332 ] {
333 if let Some(value) = value {
334 predicates.push(format!("{column}=?"));
335 values.push(value.into());
336 }
337 }
338 if filter.waiting {
339 predicates.push("q.status='WAITING_HUMAN'".into());
340 }
341 if !filter.statuses.is_empty() {
342 let marks = vec!["?"; filter.statuses.len()].join(",");
343 predicates.push(format!("q.status IN ({marks})"));
344 values.extend(
345 filter
346 .statuses
347 .iter()
348 .map(|status| rusqlite::types::Value::from(status.as_str().to_owned())),
349 );
350 }
351 let predicate = if predicates.is_empty() {
352 String::new()
353 } else {
354 format!("WHERE {}", predicates.join(" AND "))
355 };
356 let limit = match filter.slice {
357 Some((offset, count)) => {
358 values.push(i64::from(count).into());
359 values.push(i64::try_from(offset).unwrap_or(i64::MAX).into());
360 " LIMIT ? OFFSET ?"
361 }
362 None => "",
363 };
364 let order = if filter.run.is_some() {
365 "q.ordinal"
366 } else {
367 "r.rowid DESC,q.ordinal"
368 };
369 let mut statement = db.prepare_cached(&format!(
370 "SELECT q.id,q.run_id,q.data,e.data,h.id,h.claimed_by,h.claimed_at,q.eval_id,q.ordinal,e.id
371 FROM requests q JOIN runs r ON r.id=q.run_id
372 LEFT JOIN executions e ON e.id=q.execution_id
373 LEFT JOIN requests h ON h.id=json_extract(e.data,'$.provenance.requestId') AND h.claimed_by IS NOT NULL
374 {predicate} ORDER BY {order}{limit}"
375 ))?;
376 Ok(statement
377 .query_map(rusqlite::params_from_iter(values), |row| {
378 let id = row.get::<_, Option<crate::types::RequestId>>(4)?;
379 let reviewer = row.get::<_, Option<crate::types::ReviewerId>>(5)?;
380 let at = row.get::<_, Option<crate::types::Timestamp>>(6)?;
381 Ok(Row {
382 id: row.get(0)?,
383 run: row.get(1)?,
384 eval: row.get(7)?,
385 ordinal: row.get(8)?,
386 data: row.get(2)?,
387 execution: row.get(3)?,
388 execution_id: row.get(9)?,
389 claim: id
390 .zip(reviewer)
391 .zip(at)
392 .map(|((id, reviewer), at)| (id, reviewer, at)),
393 })
394 })?
395 .collect::<Result<_, _>>()?)
396}
397
398pub(super) struct DefinitionIndex {
401 artifacts: std::collections::BTreeMap<crate::types::ArtifactName, super::definitions::Artifact>,
402 evals: std::collections::BTreeMap<crate::types::EvalId, super::definitions::Eval>,
403}
404impl DefinitionIndex {
405 pub fn new(run: &super::Run) -> Self {
406 let mut evals = std::collections::BTreeMap::new();
407 if let Some(graph) = run.definitions.graph() {
408 for eval in graph.evals() {
409 evals.entry(eval.id.clone()).or_insert_with(|| eval.clone());
410 }
411 }
412 Self {
413 artifacts: run
414 .definitions
415 .graph()
416 .map(|graph| {
417 graph
418 .artifacts()
419 .map(|(id, artifact)| (id.clone(), artifact.clone()))
420 .collect()
421 })
422 .unwrap_or_default(),
423 evals,
424 }
425 }
426}
427impl Row {
428 pub fn view(&self, index: &DefinitionIndex) -> Option<RequestView> {
429 let request = super::unreadable::evidence::<Request>("request", &self.data)?;
430 let execution = self
431 .execution
432 .as_deref()
433 .and_then(|data| super::unreadable::evidence("execution", data));
434 Some(self.with_evidence(index, request, execution))
435 }
436 pub fn view_cached(
437 &self,
438 index: &DefinitionIndex,
439 audit: &mut super::unreadable::Audit,
440 ) -> Option<RequestView> {
441 let request = audit
442 .take_request(&self.id, &self.data)
443 .or_else(|| super::unreadable::evidence("request", &self.data))?;
444 let execution = self.execution.as_deref().and_then(|data| {
445 self.execution_id
446 .as_ref()
447 .and_then(|id| audit.take_execution(id, data))
448 .or_else(|| super::unreadable::evidence("execution", data))
449 });
450 Some(self.with_evidence(index, request, execution))
451 }
452 fn with_evidence(
453 &self,
454 index: &DefinitionIndex,
455 request: Request,
456 execution: Option<Execution>,
457 ) -> RequestView {
458 let definition = index
459 .evals
460 .get(&self.eval)
461 .zip(index.artifacts.get(&request.target))
462 .map(|(eval, artifact)| RequestDefinition {
463 eval: eval.clone(),
464 artifact: artifact.clone(),
465 });
466 RequestView {
467 request,
468 definition,
469 execution,
470 claim: self.claim.as_ref().map(|(id, reviewer, at)| HumanClaim {
471 request_id: id.clone(),
472 reviewer: reviewer.clone(),
473 claimed_at: *at,
474 }),
475 }
476 }
477}
478
479pub async fn read_run_requests(
481 state: &Path,
482 id: &str,
483) -> Result<(super::RunView, Vec<RequestView>), String> {
484 let state = state.to_path_buf();
485 let id: crate::types::RunId = id.parse()?;
486 crate::task::joined(
487 tokio::task::spawn_blocking(move || {
488 let mut reader = super::reader::Reader::default();
489 reader
490 .snapshot(&state, |db, cache, state| {
491 cache.selected(db, state, &id, None, true)
492 })?
493 .ok_or_else(|| "Run not found.".into())
494 })
495 .await,
496 )
497 .map_err(|error| error.to_string())?
498}