vs_store/store/
actions.rs1use rusqlite::params;
4
5use super::Store;
6use crate::error::Result;
7use crate::types::{Action, ActionFilter, ActionInsert};
8
9impl Store {
10 pub fn record_action(&mut self, ins: &ActionInsert) -> Result<i64> {
12 self.conn().execute(
13 "INSERT INTO actions(
14 session_id, page_id, primitive, args_redacted, args_hash,
15 before_token, after_token, idempotency_hit, result_summary,
16 latency_ms, group_label, started_at, finished_at, error_code
17 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
18 params![
19 ins.session_id,
20 ins.page_id,
21 ins.primitive,
22 ins.args_redacted,
23 ins.args_hash,
24 ins.before_token,
25 ins.after_token,
26 i64::from(ins.idempotency_hit),
27 ins.result_summary,
28 ins.latency_ms,
29 ins.group_label,
30 ins.started_at,
31 ins.finished_at,
32 ins.error_code,
33 ],
34 )?;
35 Ok(self.conn().last_insert_rowid())
36 }
37
38 pub fn lookup_idempotent(
44 &self,
45 page_id: &str,
46 before_token: &str,
47 args_hash: &str,
48 now_secs: i64,
49 ttl_secs: i64,
50 ) -> Result<Option<Action>> {
51 let mut stmt = self.conn().prepare(
52 "SELECT * FROM actions
53 WHERE page_id=?1 AND before_token=?2 AND args_hash=?3
54 AND error_code IS NULL
55 AND idempotency_hit=0
56 AND started_at >= ?4
57 ORDER BY started_at DESC
58 LIMIT 1",
59 )?;
60 let cutoff = now_secs - ttl_secs;
61 let mut rows = stmt.query(params![page_id, before_token, args_hash, cutoff])?;
62 if let Some(row) = rows.next()? {
63 Ok(Some(Action::from_row(row)?))
64 } else {
65 Ok(None)
66 }
67 }
68
69 pub fn list_actions(&self, filter: &ActionFilter) -> Result<Vec<Action>> {
70 use std::fmt::Write as _;
71 let mut sql = String::from("SELECT * FROM actions WHERE 1=1");
72 let mut args: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
73 let mut idx = 1usize;
74 if let Some(s) = &filter.session_id {
75 write!(sql, " AND session_id=?{idx}").expect("write to String");
76 args.push(Box::new(s.clone()));
77 idx += 1;
78 }
79 if let Some(p) = &filter.page_id {
80 write!(sql, " AND page_id=?{idx}").expect("write to String");
81 args.push(Box::new(p.clone()));
82 idx += 1;
83 }
84 if let Some(g) = &filter.group_label {
85 write!(sql, " AND group_label=?{idx}").expect("write to String");
86 args.push(Box::new(g.clone()));
87 idx += 1;
88 }
89 if let Some(t) = filter.since_started_at {
90 write!(sql, " AND started_at >= ?{idx}").expect("write to String");
91 args.push(Box::new(t));
92 idx += 1;
93 }
94 sql.push_str(" ORDER BY started_at ASC");
95 if let Some(limit) = filter.limit {
96 write!(sql, " LIMIT ?{idx}").expect("write to String");
97 args.push(Box::new(limit));
98 }
99 let mut stmt = self.conn().prepare(&sql)?;
100 let arg_refs: Vec<&dyn rusqlite::ToSql> =
101 args.iter().map(std::convert::AsRef::as_ref).collect();
102 let rows = stmt.query_map(arg_refs.as_slice(), Action::from_row)?;
103 Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
104 }
105}