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 TaskStore {
25 pub async fn get_job_state(
26 &self,
27 repo: &str,
28 job_id: &str,
29 ) -> anyhow::Result<Option<JobState>> {
30 let sql = format!("SELECT {JOB_STATE_COLS} FROM job_state WHERE repo = ? AND job_id = ?");
31 let row: Option<JobState> = sqlx::query_as(sqlx::AssertSqlSafe(sql.as_str()))
32 .bind(repo)
33 .bind(job_id)
34 .fetch_optional(&self.pool)
35 .await?;
36 Ok(row)
37 }
38
39 pub async fn upsert_job_state(&self, state: &JobState) -> anyhow::Result<()> {
41 self.write(Command::JobStateUpsert(state.clone()))
42 .await
43 .map(|_| ())
44 }
45
46 pub async fn list_job_states(&self, repo: &str) -> anyhow::Result<Vec<JobState>> {
48 let sql = format!("SELECT {JOB_STATE_COLS} FROM job_state WHERE repo = ?");
49 let rows: Vec<JobState> = sqlx::query_as(sqlx::AssertSqlSafe(sql.as_str()))
50 .bind(repo)
51 .fetch_all(&self.pool)
52 .await?;
53 Ok(rows)
54 }
55
56 pub async fn subscribe_channel(
62 &self,
63 channel: &str,
64 thread_id: &str,
65 repo: &str,
66 topic_id: Option<&str>,
67 ) -> anyhow::Result<()> {
68 let cmd = Command::ChannelSubscribe {
69 channel: channel.to_string(),
70 thread_id: thread_id.to_string(),
71 repo: repo.to_string(),
72 topic_id: topic_id.unwrap_or("").to_string(),
73 };
74 self.write(cmd).await.map(|_| ())
75 }
76
77 pub async fn unsubscribe_channel(
79 &self,
80 channel: &str,
81 thread_id: &str,
82 repo: &str,
83 ) -> anyhow::Result<()> {
84 let cmd = Command::ChannelUnsubscribe {
85 channel: channel.to_string(),
86 thread_id: thread_id.to_string(),
87 repo: repo.to_string(),
88 };
89 self.write(cmd).await.map(|_| ())
90 }
91
92 #[cfg(test)]
94 pub async fn list_channel_subscriptions(
95 &self,
96 channel: &str,
97 thread_id: &str,
98 ) -> anyhow::Result<Vec<String>> {
99 let rows: Vec<(String,)> = sqlx::query_as(
100 "SELECT repo FROM channel_subscriptions WHERE channel = ? AND thread_id = ?",
101 )
102 .bind(channel)
103 .bind(thread_id)
104 .fetch_all(&self.pool)
105 .await?;
106 Ok(rows.into_iter().map(|r| r.0).collect())
107 }
108
109 pub async fn list_subscribers_for_repo(
111 &self,
112 repo: &str,
113 ) -> anyhow::Result<Vec<(String, String, String)>> {
114 let rows: Vec<(String, String, String)> = sqlx::query_as(
115 "SELECT channel, thread_id, topic_id FROM channel_subscriptions WHERE repo = ?",
116 )
117 .bind(repo)
118 .fetch_all(&self.pool)
119 .await?;
120 Ok(rows)
121 }
122}
123
124pub(crate) mod apply {
126 use super::{JobState, SqliteConnection};
127
128 pub(crate) async fn upsert_job_state(
129 conn: &mut SqliteConnection,
130 state: &JobState,
131 ) -> anyhow::Result<()> {
132 sqlx::query(
133 "INSERT INTO job_state (repo, job_id, last_run, last_task_status, active_task_id)
134 VALUES (?, ?, ?, ?, ?)
135 ON CONFLICT(repo, job_id) DO UPDATE SET
136 last_run = excluded.last_run,
137 last_task_status = excluded.last_task_status,
138 active_task_id = excluded.active_task_id",
139 )
140 .bind(&state.repo)
141 .bind(&state.job_id)
142 .bind(&state.last_run)
143 .bind(&state.last_task_status)
144 .bind(&state.active_task_id)
145 .execute(conn)
146 .await?;
147 Ok(())
148 }
149
150 pub(crate) async fn subscribe(
151 conn: &mut SqliteConnection,
152 channel: &str,
153 thread_id: &str,
154 repo: &str,
155 topic_id: &str,
156 ) -> anyhow::Result<()> {
157 sqlx::query(
158 "INSERT OR IGNORE INTO channel_subscriptions (channel, thread_id, repo, topic_id) VALUES (?, ?, ?, ?)",
159 )
160 .bind(channel)
161 .bind(thread_id)
162 .bind(repo)
163 .bind(topic_id)
164 .execute(conn)
165 .await?;
166 Ok(())
167 }
168
169 pub(crate) async fn unsubscribe(
170 conn: &mut SqliteConnection,
171 channel: &str,
172 thread_id: &str,
173 repo: &str,
174 ) -> anyhow::Result<()> {
175 sqlx::query(
176 "DELETE FROM channel_subscriptions WHERE channel = ? AND thread_id = ? AND repo = ?",
177 )
178 .bind(channel)
179 .bind(thread_id)
180 .bind(repo)
181 .execute(conn)
182 .await?;
183 Ok(())
184 }
185}