1use rusqlite::{params, Connection, OptionalExtension, Row};
2
3#[derive(Debug, Clone)]
4pub struct BashTaskRow {
5 pub harness: String,
6 pub session_id: String,
7 pub task_id: String,
8 pub project_key: String,
9 pub command: String,
10 pub cwd: String,
11 pub status: String,
12 pub exit_code: Option<i32>,
13 pub pid: Option<i64>,
14 pub pgid: Option<i64>,
15 pub started_at: i64,
16 pub completed_at: Option<i64>,
17 pub stdout_path: Option<String>,
18 pub stderr_path: Option<String>,
19 pub compressed: bool,
20 pub timeout_ms: Option<i64>,
21 pub completion_delivered: bool,
22 pub output_bytes: Option<i64>,
23 pub metadata: String,
24}
25
26pub fn upsert_bash_task(conn: &Connection, row: &BashTaskRow) -> rusqlite::Result<()> {
27 conn.execute(
28 "INSERT INTO bash_tasks (
29 harness, session_id, task_id, project_key, command, cwd, status,
30 exit_code, pid, pgid, started_at, completed_at, stdout_path, stderr_path,
31 compressed, timeout_ms, completion_delivered, output_bytes, metadata
32 ) VALUES (
33 ?1, ?2, ?3, ?4, ?5, ?6, ?7,
34 ?8, ?9, ?10, ?11, ?12, ?13, ?14,
35 ?15, ?16, ?17, ?18, ?19
36 )
37 ON CONFLICT(harness, session_id, task_id) DO UPDATE SET
38 project_key = excluded.project_key,
39 command = excluded.command,
40 cwd = excluded.cwd,
41 status = excluded.status,
42 exit_code = excluded.exit_code,
43 pid = excluded.pid,
44 pgid = excluded.pgid,
45 started_at = excluded.started_at,
46 completed_at = excluded.completed_at,
47 stdout_path = excluded.stdout_path,
48 stderr_path = excluded.stderr_path,
49 compressed = excluded.compressed,
50 timeout_ms = excluded.timeout_ms,
51 completion_delivered = excluded.completion_delivered,
52 output_bytes = excluded.output_bytes,
53 metadata = excluded.metadata",
54 params![
55 row.harness,
56 row.session_id,
57 row.task_id,
58 row.project_key,
59 row.command,
60 row.cwd,
61 row.status,
62 row.exit_code,
63 row.pid,
64 row.pgid,
65 row.started_at,
66 row.completed_at,
67 row.stdout_path,
68 row.stderr_path,
69 row.compressed,
70 row.timeout_ms,
71 row.completion_delivered,
72 row.output_bytes,
73 row.metadata,
74 ],
75 )?;
76 Ok(())
77}
78
79pub fn delete_delivered_terminal_bash_task(
80 conn: &Connection,
81 harness: &str,
82 session_id: &str,
83 task_id: &str,
84) -> rusqlite::Result<usize> {
85 conn.execute(
86 "DELETE FROM bash_tasks
87 WHERE harness = ?1 AND session_id = ?2 AND task_id = ?3
88 AND completion_delivered = 1
89 AND status IN ('completed', 'failed', 'killed', 'timed_out', 'fate_unknown')",
90 params![harness, session_id, task_id],
91 )
92}
93
94pub fn get_bash_task(
95 conn: &Connection,
96 harness: &str,
97 session_id: &str,
98 task_id: &str,
99) -> rusqlite::Result<Option<BashTaskRow>> {
100 conn.query_row(
101 "SELECT harness, session_id, task_id, project_key, command, cwd, status,
102 exit_code, pid, pgid, started_at, completed_at, stdout_path, stderr_path,
103 compressed, timeout_ms, completion_delivered, output_bytes, metadata
104 FROM bash_tasks
105 WHERE harness = ?1 AND session_id = ?2 AND task_id = ?3",
106 params![harness, session_id, task_id],
107 map_bash_task_row,
108 )
109 .optional()
110}
111
112pub fn list_bash_tasks_for_session(
113 conn: &Connection,
114 harness: &str,
115 session_id: &str,
116) -> rusqlite::Result<Vec<BashTaskRow>> {
117 let mut stmt = conn.prepare(
118 "SELECT harness, session_id, task_id, project_key, command, cwd, status,
119 exit_code, pid, pgid, started_at, completed_at, stdout_path, stderr_path,
120 compressed, timeout_ms, completion_delivered, output_bytes, metadata
121 FROM bash_tasks
122 WHERE harness = ?1 AND session_id = ?2
123 ORDER BY started_at ASC, task_id ASC",
124 )?;
125
126 let rows = stmt
127 .query_map(params![harness, session_id], map_bash_task_row)?
128 .collect();
129 rows
130}
131
132pub fn list_bash_tasks_by_id(
133 conn: &Connection,
134 harness: &str,
135 task_id: &str,
136) -> rusqlite::Result<Vec<BashTaskRow>> {
137 let mut stmt = conn.prepare(
138 "SELECT harness, session_id, task_id, project_key, command, cwd, status,
139 exit_code, pid, pgid, started_at, completed_at, stdout_path, stderr_path,
140 compressed, timeout_ms, completion_delivered, output_bytes, metadata
141 FROM bash_tasks
142 WHERE harness = ?1 AND task_id = ?2
143 ORDER BY started_at DESC",
144 )?;
145 let rows = stmt
146 .query_map(params![harness, task_id], map_bash_task_row)?
147 .collect();
148 rows
149}
150
151pub fn list_replayable_bash_tasks_for_project(
152 conn: &Connection,
153 harness: &str,
154 project_key: &str,
155) -> rusqlite::Result<Vec<BashTaskRow>> {
156 let mut stmt = conn.prepare(
157 "SELECT harness, session_id, task_id, project_key, command, cwd, status,
158 exit_code, pid, pgid, started_at, completed_at, stdout_path, stderr_path,
159 compressed, timeout_ms, completion_delivered, output_bytes, metadata
160 FROM bash_tasks
161 WHERE harness = ?1 AND project_key = ?2
162 AND (status NOT IN ('completed', 'failed', 'killed', 'timed_out', 'fate_unknown')
163 OR completion_delivered = 0)
164 ORDER BY started_at ASC, task_id ASC",
165 )?;
166 let rows = stmt
167 .query_map(params![harness, project_key], map_bash_task_row)?
168 .collect();
169 rows
170}
171
172pub fn find_bash_task_for_project(
173 conn: &Connection,
174 harness: &str,
175 project_key: &str,
176 task_id: &str,
177) -> rusqlite::Result<Option<BashTaskRow>> {
178 conn.query_row(
179 "SELECT harness, session_id, task_id, project_key, command, cwd, status,
180 exit_code, pid, pgid, started_at, completed_at, stdout_path, stderr_path,
181 compressed, timeout_ms, completion_delivered, output_bytes, metadata
182 FROM bash_tasks
183 WHERE harness = ?1 AND project_key = ?2 AND task_id = ?3
184 ORDER BY started_at DESC
185 LIMIT 1",
186 params![harness, project_key, task_id],
187 map_bash_task_row,
188 )
189 .optional()
190}
191
192fn map_bash_task_row(row: &Row<'_>) -> rusqlite::Result<BashTaskRow> {
193 Ok(BashTaskRow {
194 harness: row.get(0)?,
195 session_id: row.get(1)?,
196 task_id: row.get(2)?,
197 project_key: row.get(3)?,
198 command: row.get(4)?,
199 cwd: row.get(5)?,
200 status: row.get(6)?,
201 exit_code: row.get(7)?,
202 pid: row.get(8)?,
203 pgid: row.get(9)?,
204 started_at: row.get(10)?,
205 completed_at: row.get(11)?,
206 stdout_path: row.get(12)?,
207 stderr_path: row.get(13)?,
208 compressed: row.get::<_, i64>(14)? != 0,
209 timeout_ms: row.get(15)?,
210 completion_delivered: row.get::<_, i64>(16)? != 0,
211 output_bytes: row.get(17)?,
212 metadata: row.get::<_, Option<String>>(18)?.unwrap_or_default(),
213 })
214}