1use super::TaskStore;
2use crate::cluster::Command;
3use serde::{Deserialize, Serialize};
4use sqlx::SqliteConnection;
5
6const JOB_STATE_COLS: &str = "repo, job_id, last_run, last_task_status, active_task_id";
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, sqlx::FromRow)]
16pub struct JobState {
17 pub repo: String,
18 pub job_id: String,
19 pub last_run: Option<String>,
20 pub last_task_status: Option<String>,
21 pub active_task_id: Option<String>,
22}
23
24impl JobState {
25 pub fn new(repo: &str, job_id: &str) -> Self {
27 Self {
28 repo: repo.to_string(),
29 job_id: job_id.to_string(),
30 last_run: None,
31 last_task_status: None,
32 active_task_id: None,
33 }
34 }
35}
36
37impl TaskStore {
38 pub async fn get_job_state(
39 &self,
40 repo: &str,
41 job_id: &str,
42 ) -> anyhow::Result<Option<JobState>> {
43 let sql = format!("SELECT {JOB_STATE_COLS} FROM job_state WHERE repo = ? AND job_id = ?");
44 let row: Option<JobState> = sqlx::query_as(sqlx::AssertSqlSafe(sql.as_str()))
45 .bind(repo)
46 .bind(job_id)
47 .fetch_optional(&self.pool)
48 .await?;
49 Ok(row)
50 }
51
52 pub async fn upsert_job_state(&self, state: &JobState) -> anyhow::Result<()> {
54 self.write(Command::JobStateUpsert(state.clone()))
55 .await
56 .map(|_| ())
57 }
58
59 pub async fn list_job_states(&self, repo: &str) -> anyhow::Result<Vec<JobState>> {
61 let sql = format!("SELECT {JOB_STATE_COLS} FROM job_state WHERE repo = ?");
62 let rows: Vec<JobState> = sqlx::query_as(sqlx::AssertSqlSafe(sql.as_str()))
63 .bind(repo)
64 .fetch_all(&self.pool)
65 .await?;
66 Ok(rows)
67 }
68
69 pub async fn subscribe_channel(
75 &self,
76 channel: &str,
77 thread_id: &str,
78 repo: &str,
79 topic_id: Option<&str>,
80 ) -> anyhow::Result<()> {
81 let cmd = Command::ChannelSubscribe {
82 channel: channel.to_string(),
83 thread_id: thread_id.to_string(),
84 repo: repo.to_string(),
85 topic_id: topic_id.unwrap_or("").to_string(),
86 };
87 self.write(cmd).await.map(|_| ())
88 }
89
90 pub async fn unsubscribe_channel(
92 &self,
93 channel: &str,
94 thread_id: &str,
95 repo: &str,
96 ) -> anyhow::Result<()> {
97 let cmd = Command::ChannelUnsubscribe {
98 channel: channel.to_string(),
99 thread_id: thread_id.to_string(),
100 repo: repo.to_string(),
101 };
102 self.write(cmd).await.map(|_| ())
103 }
104
105 #[cfg(test)]
107 pub async fn list_channel_subscriptions(
108 &self,
109 channel: &str,
110 thread_id: &str,
111 ) -> anyhow::Result<Vec<String>> {
112 let rows: Vec<(String,)> = sqlx::query_as(
113 "SELECT repo FROM channel_subscriptions WHERE channel = ? AND thread_id = ?",
114 )
115 .bind(channel)
116 .bind(thread_id)
117 .fetch_all(&self.pool)
118 .await?;
119 Ok(rows.into_iter().map(|r| r.0).collect())
120 }
121
122 pub async fn list_subscribers_for_repo(
124 &self,
125 repo: &str,
126 ) -> anyhow::Result<Vec<(String, String, String)>> {
127 let rows: Vec<(String, String, String)> = sqlx::query_as(
128 "SELECT channel, thread_id, topic_id FROM channel_subscriptions WHERE repo = ?",
129 )
130 .bind(repo)
131 .fetch_all(&self.pool)
132 .await?;
133 Ok(rows)
134 }
135}
136
137pub(crate) mod apply {
139 use super::{JobState, SqliteConnection};
140
141 pub(crate) async fn upsert_job_state(
142 conn: &mut SqliteConnection,
143 state: &JobState,
144 ) -> anyhow::Result<()> {
145 sqlx::query(
146 "INSERT INTO job_state (repo, job_id, last_run, last_task_status, active_task_id)
147 VALUES (?, ?, ?, ?, ?)
148 ON CONFLICT(repo, job_id) DO UPDATE SET
149 last_run = excluded.last_run,
150 last_task_status = excluded.last_task_status,
151 active_task_id = excluded.active_task_id",
152 )
153 .bind(&state.repo)
154 .bind(&state.job_id)
155 .bind(&state.last_run)
156 .bind(&state.last_task_status)
157 .bind(&state.active_task_id)
158 .execute(conn)
159 .await?;
160 Ok(())
161 }
162
163 pub(crate) async fn subscribe(
164 conn: &mut SqliteConnection,
165 channel: &str,
166 thread_id: &str,
167 repo: &str,
168 topic_id: &str,
169 ) -> anyhow::Result<()> {
170 sqlx::query(
171 "INSERT OR IGNORE INTO channel_subscriptions (channel, thread_id, repo, topic_id) VALUES (?, ?, ?, ?)",
172 )
173 .bind(channel)
174 .bind(thread_id)
175 .bind(repo)
176 .bind(topic_id)
177 .execute(conn)
178 .await?;
179 Ok(())
180 }
181
182 pub(crate) async fn unsubscribe(
183 conn: &mut SqliteConnection,
184 channel: &str,
185 thread_id: &str,
186 repo: &str,
187 ) -> anyhow::Result<()> {
188 sqlx::query(
189 "DELETE FROM channel_subscriptions WHERE channel = ? AND thread_id = ? AND repo = ?",
190 )
191 .bind(channel)
192 .bind(thread_id)
193 .bind(repo)
194 .execute(conn)
195 .await?;
196 Ok(())
197 }
198}