1use super::{
4 RunStore,
5 codec::{
6 ACTIVITY_COLUMNS, LIFECYCLE_COLUMNS, LINK_COLUMNS, counter, read_activity, read_event,
7 read_link, read_run,
8 },
9};
10use crate::{
11 RunStoreError, StoreError, StoredRecord,
12 error::database_error,
13 sqlite::snapshot::{decode, revision_to_sql},
14};
15use dpm_model::{
16 ActivityEntry, ActivityGap, ActivityPage, ActivityTally, LatestActivity, LifecycleEntry,
17 LifecyclePage, RunId, RunLink, RunRecord, RunSnapshot, WorkItemId,
18};
19use rusqlite::{Connection, OptionalExtension, params};
20use std::path::Path;
21
22pub(super) const PAGE_LIMIT: u16 = 1000;
24
25pub(super) fn load_run(
26 connection: &Connection,
27 path: &Path,
28 id: RunId,
29) -> Result<Option<RunRecord>, StoreError> {
30 connection
31 .query_row(
32 "SELECT record_json FROM runs WHERE run_id = ?1",
33 params![id.to_string()],
34 |row| Ok(read_run(row, path)),
35 )
36 .optional()
37 .map_err(database_error(path, "look up run"))?
38 .transpose()
39}
40
41pub(super) fn last_event(
42 connection: &Connection,
43 path: &Path,
44 run: RunId,
45) -> Result<Option<LifecycleEntry>, StoreError> {
46 connection
47 .query_row(
48 &format!("{LIFECYCLE_COLUMNS} WHERE run_id = ?1 ORDER BY sequence DESC LIMIT 1"),
49 params![run.to_string()],
50 |row| Ok(read_event(row, path)),
51 )
52 .optional()
53 .map_err(database_error(path, "look up run state"))?
54 .transpose()
55}
56
57pub(super) fn event_by_id(
58 connection: &Connection,
59 path: &Path,
60 id: dpm_model::RunEventId,
61) -> Result<Option<LifecycleEntry>, StoreError> {
62 connection
63 .query_row(
64 &format!("{LIFECYCLE_COLUMNS} WHERE event_id = ?1"),
65 params![id.to_string()],
66 |row| Ok(read_event(row, path)),
67 )
68 .optional()
69 .map_err(database_error(path, "look up transition"))?
70 .transpose()
71}
72
73pub(super) fn activity_by_sequence(
74 connection: &Connection,
75 path: &Path,
76 run: RunId,
77 source_sequence: u64,
78) -> Result<Option<ActivityEntry>, StoreError> {
79 connection
80 .query_row(
81 &format!("{ACTIVITY_COLUMNS} WHERE run_id = ?1 AND source_sequence = ?2"),
82 params![run.to_string(), revision_to_sql(source_sequence)],
83 |row| Ok(read_activity(row, path)),
84 )
85 .optional()
86 .map_err(database_error(path, "look up activity"))?
87 .transpose()
88}
89
90pub(super) struct TallyRow {
93 pub(super) recorded: u64,
94 pub(super) high_water: u64,
95}
96
97pub(super) fn tally_row(
98 connection: &Connection,
99 path: &Path,
100 run: RunId,
101) -> Result<Option<TallyRow>, StoreError> {
102 connection
103 .query_row(
104 "SELECT recorded, high_water FROM run_activity_tally WHERE run_id = ?1",
105 params![run.to_string()],
106 |row| Ok(read_tally(row, path)),
107 )
108 .optional()
109 .map_err(database_error(path, "read run tally"))?
110 .transpose()
111}
112
113fn read_tally(row: &rusqlite::Row<'_>, path: &Path) -> Result<TallyRow, StoreError> {
114 Ok(TallyRow {
115 recorded: counter(row, 0, path, "recorded")?,
116 high_water: counter(row, 1, path, "high_water")?,
117 })
118}
119
120pub(super) fn link_by_operation(
121 connection: &Connection,
122 path: &Path,
123 operation: dpm_model::OperationId,
124) -> Result<Option<RunLink>, StoreError> {
125 connection
126 .query_row(
127 &format!("{LINK_COLUMNS} WHERE operation_id = ?1"),
128 params![operation.to_string()],
129 |row| Ok(read_link(row, path)),
130 )
131 .optional()
132 .map_err(database_error(path, "look up run link"))?
133 .transpose()
134}
135
136fn links_of(connection: &Connection, path: &Path, run: RunId) -> Result<Vec<RunLink>, StoreError> {
137 let mut statement = connection
138 .prepare(&format!(
139 "{LINK_COLUMNS} WHERE run_id = ?1 ORDER BY linked_at, operation_id"
140 ))
141 .map_err(database_error(path, "prepare run links"))?;
142 let mut rows = statement
143 .query(params![run.to_string()])
144 .map_err(database_error(path, "read run links"))?;
145 let mut links = Vec::new();
146 while let Some(row) = rows
147 .next()
148 .map_err(database_error(path, "read run links"))?
149 {
150 links.push(read_link(row, path)?);
151 }
152 Ok(links)
153}
154
155fn tally(connection: &Connection, path: &Path, run: RunId) -> Result<ActivityTally, StoreError> {
156 let retained: u64 = connection
157 .query_row(
158 "SELECT COUNT(*) FROM run_activity WHERE run_id = ?1",
159 params![run.to_string()],
160 |row| row.get(0),
161 )
162 .map_err(database_error(path, "count run activity"))?;
163 let Some(counts) = tally_row(connection, path, run)? else {
164 return Ok(ActivityTally {
165 recorded: 0,
166 retained,
167 source_high_water: 0,
168 latest: None,
169 });
170 };
171 let (kind, at): (String, String) = connection
172 .query_row(
173 "SELECT last_kind, last_at FROM run_activity_tally WHERE run_id = ?1",
174 params![run.to_string()],
175 |row| Ok((row.get(0)?, row.get(1)?)),
176 )
177 .map_err(database_error(path, "read latest activity"))?;
178 let scalar = |value: String| serde_json::Value::String(value).to_string();
179 Ok(ActivityTally {
180 recorded: counts.recorded,
181 retained,
182 source_high_water: counts.high_water,
183 latest: Some(LatestActivity {
184 kind: decode(&scalar(kind), path, StoredRecord::Run, "last_kind")?,
185 source_sequence: counts.high_water,
186 recorded_at: decode(&scalar(at), path, StoredRecord::Run, "last_at")?,
187 }),
188 })
189}
190
191pub(super) fn snapshot(
193 connection: &Connection,
194 path: &Path,
195 id: RunId,
196) -> Result<Option<RunSnapshot>, StoreError> {
197 let Some(record) = load_run(connection, path, id)? else {
198 return Ok(None);
199 };
200 let last = last_event(connection, path, id)?.ok_or_else(|| StoreError::CorruptJson {
201 path: path.to_path_buf(),
202 record: StoredRecord::Run,
203 field: "run_lifecycle",
204 source: missing_start(id),
205 })?;
206 Ok(Some(RunSnapshot {
207 operations: links_of(connection, path, id)?,
208 activity: tally(connection, path, id)?,
209 last: last.event,
210 record,
211 }))
212}
213
214fn missing_start(id: RunId) -> serde_json::Error {
216 <serde_json::Error as serde::de::Error>::custom(format!("run {id} has no lifecycle record"))
217}
218
219impl RunStore {
220 pub fn heads_blocking(&self) -> Result<dpm_model::RunFeedHeads, RunStoreError> {
223 let transaction = self.read_transaction()?;
224 let binding = super::read_binding(&transaction, &self.path)?;
225 Ok(dpm_model::RunFeedHeads {
226 epoch: Some(binding.lineage_id),
227 lifecycle_head: head(&transaction, &self.path, "run_lifecycle")?,
228 activity_head: head(&transaction, &self.path, "run_activity")?,
229 activity_pruned_through: pruned_through(&transaction, &self.path)?,
230 link_count: link_count_blocking(&transaction, &self.path)?,
231 })
232 }
233
234 pub fn snapshot_blocking(&self, id: RunId) -> Result<Option<RunSnapshot>, RunStoreError> {
236 let transaction = self.read_transaction()?;
237 Ok(snapshot(&transaction, &self.path, id)?)
238 }
239
240 pub fn snapshots_blocking(
242 &self,
243 work: Option<WorkItemId>,
244 limit: u16,
245 ) -> Result<Vec<RunSnapshot>, RunStoreError> {
246 let transaction = self.read_transaction()?;
247 let mut statement = transaction
248 .prepare(
249 "SELECT run_id FROM runs WHERE (?1 IS NULL OR work_id = ?1) \
250 ORDER BY rowid DESC LIMIT ?2",
251 )
252 .map_err(database_error(&self.path, "prepare run list"))?;
253 let ids = statement
254 .query_map(
255 params![work.map(|id| id.to_string()), limit.min(PAGE_LIMIT)],
256 |row| row.get::<_, String>(0),
257 )
258 .map_err(database_error(&self.path, "list runs"))?
259 .collect::<Result<Vec<_>, _>>()
260 .map_err(database_error(&self.path, "read run list"))?;
261 let mut runs = Vec::with_capacity(ids.len());
262 for text in ids {
263 let id: RunId = decode(
264 &serde_json::Value::String(text).to_string(),
265 &self.path,
266 StoredRecord::Run,
267 "run_id",
268 )?;
269 runs.extend(snapshot(&transaction, &self.path, id)?);
270 }
271 Ok(runs)
272 }
273
274 pub fn lifecycle_page_blocking(
277 &self,
278 after_sequence: u64,
279 limit: u16,
280 run: Option<RunId>,
281 ) -> Result<LifecyclePage, RunStoreError> {
282 let transaction = self.read_transaction()?;
283 let mut entries = Vec::new();
284 if let Ok(cursor) = i64::try_from(after_sequence) {
285 let mut statement = transaction
286 .prepare(&format!(
287 "{LIFECYCLE_COLUMNS} WHERE sequence > ?1 AND (?2 IS NULL OR run_id = ?2) \
288 ORDER BY sequence LIMIT ?3"
289 ))
290 .map_err(database_error(&self.path, "prepare lifecycle feed"))?;
291 let mut rows = statement
292 .query(params![
293 cursor,
294 run.map(|id| id.to_string()),
295 limit.min(PAGE_LIMIT)
296 ])
297 .map_err(database_error(&self.path, "read lifecycle feed"))?;
298 while let Some(row) = rows
299 .next()
300 .map_err(database_error(&self.path, "read lifecycle feed"))?
301 {
302 entries.push(read_event(row, &self.path)?);
303 }
304 }
305 let head = head(&transaction, &self.path, "run_lifecycle")?;
306 Ok(LifecyclePage {
307 next_after_sequence: entries
308 .last()
309 .map_or(after_sequence, |entry| entry.sequence),
310 entries,
311 head_sequence: head,
312 })
313 }
314
315 pub fn activity_page_blocking(
321 &self,
322 after_sequence: u64,
323 limit: u16,
324 run: Option<RunId>,
325 ) -> Result<ActivityPage, RunStoreError> {
326 let transaction = self.read_transaction()?;
327 let pruned = pruned_through(&transaction, &self.path)?;
328 let mut entries = Vec::new();
329 if let Ok(cursor) = i64::try_from(after_sequence.max(pruned)) {
330 let mut statement = transaction
331 .prepare(&format!(
332 "{ACTIVITY_COLUMNS} WHERE sequence > ?1 AND (?2 IS NULL OR run_id = ?2) \
333 ORDER BY sequence LIMIT ?3"
334 ))
335 .map_err(database_error(&self.path, "prepare activity feed"))?;
336 let mut rows = statement
337 .query(params![
338 cursor,
339 run.map(|id| id.to_string()),
340 limit.min(PAGE_LIMIT)
341 ])
342 .map_err(database_error(&self.path, "read activity feed"))?;
343 while let Some(row) = rows
344 .next()
345 .map_err(database_error(&self.path, "read activity feed"))?
346 {
347 entries.push(read_activity(row, &self.path)?);
348 }
349 }
350 let gap = (after_sequence < pruned).then(|| ActivityGap {
351 requested_after: after_sequence,
352 resumes_at: pruned.saturating_add(1),
353 lost: pruned.saturating_sub(after_sequence),
354 });
355 let resumed = gap.map_or(after_sequence, |_| pruned);
356 Ok(ActivityPage {
357 next_after_sequence: entries.last().map_or(resumed, |entry| entry.sequence),
358 entries,
359 head_sequence: head(&transaction, &self.path, "run_activity")?,
360 gap,
361 })
362 }
363}
364
365pub(super) fn pruned_through(connection: &Connection, path: &Path) -> Result<u64, StoreError> {
367 connection
368 .query_row(
369 "SELECT pruned_through FROM run_activity_retention WHERE singleton = 1",
370 [],
371 |row| row.get(0),
372 )
373 .map_err(database_error(path, "read activity retention"))
374}
375
376fn link_count_blocking(connection: &Connection, path: &Path) -> Result<u64, StoreError> {
383 connection
384 .query_row("SELECT COUNT(*) FROM run_links", [], |row| row.get(0))
385 .map_err(database_error(path, "count run links"))
386}
387
388pub(super) fn head(
390 connection: &Connection,
391 path: &Path,
392 table: &'static str,
393) -> Result<u64, StoreError> {
394 connection
395 .query_row(
396 "SELECT seq FROM sqlite_sequence WHERE name = ?1",
397 params![table],
398 |row| row.get(0),
399 )
400 .optional()
401 .map_err(database_error(path, "read feed head"))
402 .map(Option::unwrap_or_default)
403}