1use chrono::{DateTime, Utc};
2use sqlx::{Row, SqlitePool, sqlite::SqliteRow};
3
4use crate::{
5 convert::{enum_to_text, option_enum_text, text_to_enum},
6 error::{DbError, DbResult},
7};
8
9use super::tune_samples::SampleQuality;
10
11#[cfg(doc)]
12use super::tune_samples::TuneSampleRow;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
18#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
19#[serde(rename_all = "snake_case")]
20pub enum MvActuationKind {
21 Relay,
23 Restore,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
29#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
30#[serde(rename_all = "snake_case")]
31pub enum MvActuationStatus {
32 Pending,
34 Confirmed,
36 Failed,
38 Unverified,
41 Superseded,
44}
45
46impl MvActuationStatus {
47 fn ensure_terminal(self) -> DbResult<()> {
48 if self == MvActuationStatus::Pending {
49 Err(DbError::InvalidMvActuationFinalStatus)
50 } else {
51 Ok(())
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq)]
63pub struct NewTuneMvActuation {
64 pub sequence: i64,
65 pub kind: MvActuationKind,
66 pub commanded_at: DateTime<Utc>,
67 pub target_mv: f32,
68 pub previous_commanded_mv: Option<f32>,
71 pub tolerance: f32,
72 pub confirmation_due_at: DateTime<Utc>,
73}
74
75#[derive(Debug, Clone, PartialEq, serde::Serialize)]
83pub struct TuneMvActuationRow {
84 pub id: i64,
85 pub run_id: i64,
86 pub sequence: i64,
88 pub kind: MvActuationKind,
89 pub commanded_at: DateTime<Utc>,
91 pub target_mv: f32,
92 pub previous_commanded_mv: Option<f32>,
93 pub tolerance: f32,
95 pub confirmation_due_at: DateTime<Utc>,
97 pub last_checked_at: Option<DateTime<Utc>>,
98 pub readback_mv: Option<f32>,
99 pub readback_quality: Option<SampleQuality>,
100 pub attempt_count: i64,
101 pub status: MvActuationStatus,
102 pub detail: Option<String>,
104}
105
106impl TuneMvActuationRow {
107 pub async fn insert_pending(
111 pool: &SqlitePool,
112 run_id: i64,
113 new: NewTuneMvActuation,
114 ) -> DbResult<TuneMvActuationRow> {
115 let row = sqlx::query(
116 r"
117 INSERT INTO tune_mv_actuations (
118 run_id, sequence, kind, commanded_at, target_mv, previous_commanded_mv,
119 tolerance, confirmation_due_at
120 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
121 RETURNING *
122 ",
123 )
124 .bind(run_id)
125 .bind(new.sequence)
126 .bind(enum_to_text(&new.kind)?)
127 .bind(new.commanded_at)
128 .bind(new.target_mv)
129 .bind(new.previous_commanded_mv)
130 .bind(new.tolerance)
131 .bind(new.confirmation_due_at)
132 .fetch_one(pool)
133 .await
134 .map_err(DbError::Query)?;
135
136 row_to_tune_mv_actuation(row)
137 }
138
139 pub async fn record_observation(
143 pool: &SqlitePool,
144 id: i64,
145 checked_at: DateTime<Utc>,
146 readback_mv: Option<f32>,
147 readback_quality: Option<SampleQuality>,
148 ) -> DbResult<TuneMvActuationRow> {
149 let row = sqlx::query(
150 r"
151 UPDATE tune_mv_actuations
152 SET last_checked_at = ?, readback_mv = ?, readback_quality = ?,
153 attempt_count = attempt_count + 1
154 WHERE id = ? AND status = 'pending'
155 RETURNING *
156 ",
157 )
158 .bind(checked_at)
159 .bind(readback_mv)
160 .bind(option_enum_text(readback_quality.as_ref())?)
161 .bind(id)
162 .fetch_one(pool)
163 .await
164 .map_err(DbError::Query)?;
165
166 row_to_tune_mv_actuation(row)
167 }
168
169 pub async fn record_final_observation(
174 pool: &SqlitePool,
175 id: i64,
176 checked_at: DateTime<Utc>,
177 readback_mv: Option<f32>,
178 readback_quality: Option<SampleQuality>,
179 status: MvActuationStatus,
180 detail: Option<&str>,
181 ) -> DbResult<TuneMvActuationRow> {
182 status.ensure_terminal()?;
183 let row = sqlx::query(
184 r"
185 UPDATE tune_mv_actuations
186 SET last_checked_at = ?, readback_mv = ?, readback_quality = ?,
187 attempt_count = attempt_count + 1, status = ?, detail = ?
188 WHERE id = ? AND status = 'pending'
189 RETURNING *
190 ",
191 )
192 .bind(checked_at)
193 .bind(readback_mv)
194 .bind(option_enum_text(readback_quality.as_ref())?)
195 .bind(enum_to_text(&status)?)
196 .bind(detail)
197 .bind(id)
198 .fetch_one(pool)
199 .await
200 .map_err(DbError::Query)?;
201
202 row_to_tune_mv_actuation(row)
203 }
204
205 pub async fn finalize(
208 pool: &SqlitePool,
209 id: i64,
210 status: MvActuationStatus,
211 detail: Option<&str>,
212 ) -> DbResult<TuneMvActuationRow> {
213 status.ensure_terminal()?;
214 let row = sqlx::query(
215 r"
216 UPDATE tune_mv_actuations
217 SET status = ?, detail = ?
218 WHERE id = ? AND status = 'pending'
219 RETURNING *
220 ",
221 )
222 .bind(enum_to_text(&status)?)
223 .bind(detail)
224 .bind(id)
225 .fetch_one(pool)
226 .await
227 .map_err(DbError::Query)?;
228
229 row_to_tune_mv_actuation(row)
230 }
231
232 pub async fn finalize_pending_for_run(
237 pool: &SqlitePool,
238 run_id: i64,
239 status: MvActuationStatus,
240 detail: Option<&str>,
241 ) -> DbResult<u64> {
242 status.ensure_terminal()?;
243 let result = sqlx::query(
244 r"
245 UPDATE tune_mv_actuations
246 SET status = ?, detail = ?
247 WHERE run_id = ? AND status = 'pending'
248 ",
249 )
250 .bind(enum_to_text(&status)?)
251 .bind(detail)
252 .bind(run_id)
253 .execute(pool)
254 .await
255 .map_err(DbError::Query)?;
256 Ok(result.rows_affected())
257 }
258
259 pub async fn list_for_run(pool: &SqlitePool, run_id: i64) -> DbResult<Vec<TuneMvActuationRow>> {
261 let rows =
262 sqlx::query("SELECT * FROM tune_mv_actuations WHERE run_id = ? ORDER BY sequence, id")
263 .bind(run_id)
264 .fetch_all(pool)
265 .await
266 .map_err(DbError::Query)?;
267 rows.into_iter().map(row_to_tune_mv_actuation).collect()
268 }
269}
270
271fn row_to_tune_mv_actuation(row: SqliteRow) -> DbResult<TuneMvActuationRow> {
272 let kind: String = row.try_get("kind").map_err(DbError::Query)?;
273 let readback_quality: Option<String> =
274 row.try_get("readback_quality").map_err(DbError::Query)?;
275 let status: String = row.try_get("status").map_err(DbError::Query)?;
276 Ok(TuneMvActuationRow {
277 id: row.try_get("id").map_err(DbError::Query)?,
278 run_id: row.try_get("run_id").map_err(DbError::Query)?,
279 sequence: row.try_get("sequence").map_err(DbError::Query)?,
280 kind: text_to_enum("kind", &kind)?,
281 commanded_at: row.try_get("commanded_at").map_err(DbError::Query)?,
282 target_mv: row.try_get("target_mv").map_err(DbError::Query)?,
283 previous_commanded_mv: row
284 .try_get("previous_commanded_mv")
285 .map_err(DbError::Query)?,
286 tolerance: row.try_get("tolerance").map_err(DbError::Query)?,
287 confirmation_due_at: row.try_get("confirmation_due_at").map_err(DbError::Query)?,
288 last_checked_at: row.try_get("last_checked_at").map_err(DbError::Query)?,
289 readback_mv: row.try_get("readback_mv").map_err(DbError::Query)?,
290 readback_quality: readback_quality
291 .map(|quality| text_to_enum("readback_quality", &quality))
292 .transpose()?,
293 attempt_count: row.try_get("attempt_count").map_err(DbError::Query)?,
294 status: text_to_enum("status", &status)?,
295 detail: row.try_get("detail").map_err(DbError::Query)?,
296 })
297}
298