1use bhtune_core::{DcsTemplate, PidRounding};
2use chrono::{DateTime, Utc};
3use sqlx::{Row, SqlitePool, sqlite::SqliteRow};
4
5use crate::{
6 convert::{enum_to_text, json_text, text_to_enum},
7 error::{DbError, DbResult},
8};
9
10#[cfg(doc)]
11use super::tune_runs::TuneRunRow;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
30#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
31#[serde(rename_all = "snake_case")]
32pub enum TemplateOrigin {
33 Builtin,
36 Catalog,
41 User,
44}
45
46#[derive(Debug, Clone, PartialEq)]
49pub struct DcsTemplateRow {
50 pub id: i64,
51 pub origin: TemplateOrigin,
52 pub template: DcsTemplate,
53 pub created_at: DateTime<Utc>,
54 pub updated_at: DateTime<Utc>,
55}
56
57impl DcsTemplateRow {
58 pub async fn insert(
63 pool: &SqlitePool,
64 template: &DcsTemplate,
65 origin: TemplateOrigin,
66 now: DateTime<Utc>,
67 ) -> DbResult<DcsTemplateRow> {
68 let query = sqlx::query(
69 r"
70 INSERT INTO dcs_templates (
71 name, origin, revert_mode, proportional_type, integral_type,
72 integral_unit, derivative_type, derivative_unit,
73 pid_rounding_kind, pid_rounding_digits,
74 process_variable_suffix, manipulated_variable_suffix, setpoint_variable_suffix,
75 controller_direction_suffix, controller_mode_suffix, mode_attribute_suffix,
76 upper_pv_range_suffix, lower_pv_range_suffix, upper_mv_range_suffix,
77 lower_mv_range_suffix, proportional_constant_suffix, integral_constant_suffix,
78 derivative_constant_suffix, mode_manual_value, mode_auto_value,
79 mode_attribute_program_value, controller_action_direct_value,
80 versions_json, description, source,
81 created_at, updated_at
82 )
83 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
84 RETURNING *
85 ",
86 )
87 .bind(&template.name)
88 .bind(enum_to_text(&origin)?);
89 let row = bind_shared_template_fields(query, template)?
90 .bind(now)
91 .bind(now)
92 .fetch_one(pool)
93 .await
94 .map_err(DbError::Query)?;
95
96 row_to_dcs_template(row)
97 }
98
99 pub async fn get(pool: &SqlitePool, id: i64) -> DbResult<Option<DcsTemplateRow>> {
101 let row = sqlx::query("SELECT * FROM dcs_templates WHERE id = ?")
102 .bind(id)
103 .fetch_optional(pool)
104 .await
105 .map_err(DbError::Query)?;
106 row.map(row_to_dcs_template).transpose()
107 }
108
109 pub async fn get_by_name(pool: &SqlitePool, name: &str) -> DbResult<Option<DcsTemplateRow>> {
113 let row = sqlx::query("SELECT * FROM dcs_templates WHERE name = ?")
114 .bind(name)
115 .fetch_optional(pool)
116 .await
117 .map_err(DbError::Query)?;
118 row.map(row_to_dcs_template).transpose()
119 }
120
121 pub async fn list(pool: &SqlitePool) -> DbResult<Vec<DcsTemplateRow>> {
123 let rows = sqlx::query("SELECT * FROM dcs_templates ORDER BY name")
124 .fetch_all(pool)
125 .await
126 .map_err(DbError::Query)?;
127 rows.into_iter().map(row_to_dcs_template).collect()
128 }
129
130 pub async fn update(
135 pool: &SqlitePool,
136 id: i64,
137 template: &DcsTemplate,
138 now: DateTime<Utc>,
139 ) -> DbResult<DcsTemplateRow> {
140 let query = sqlx::query(
141 r"
142 UPDATE dcs_templates SET
143 revert_mode = ?, proportional_type = ?, integral_type = ?,
144 integral_unit = ?, derivative_type = ?, derivative_unit = ?,
145 pid_rounding_kind = ?, pid_rounding_digits = ?,
146 process_variable_suffix = ?, manipulated_variable_suffix = ?,
147 setpoint_variable_suffix = ?, controller_direction_suffix = ?,
148 controller_mode_suffix = ?, mode_attribute_suffix = ?,
149 upper_pv_range_suffix = ?, lower_pv_range_suffix = ?,
150 upper_mv_range_suffix = ?, lower_mv_range_suffix = ?,
151 proportional_constant_suffix = ?, integral_constant_suffix = ?,
152 derivative_constant_suffix = ?, mode_manual_value = ?, mode_auto_value = ?,
153 mode_attribute_program_value = ?, controller_action_direct_value = ?,
154 versions_json = ?, description = ?, source = ?,
155 updated_at = ?
156 WHERE id = ?
157 RETURNING *
158 ",
159 );
160 let row = bind_shared_template_fields(query, template)?
161 .bind(now)
162 .bind(id)
163 .fetch_one(pool)
164 .await
165 .map_err(DbError::Query)?;
166
167 row_to_dcs_template(row)
168 }
169
170 pub async fn delete(pool: &SqlitePool, id: i64) -> DbResult<bool> {
192 let result = sqlx::query("DELETE FROM dcs_templates WHERE id = ?")
193 .bind(id)
194 .execute(pool)
195 .await
196 .map_err(|e| {
197 if e.as_database_error().is_some() {
198 DbError::TemplateInUse { id }
199 } else {
200 DbError::Query(e)
201 }
202 })?;
203 Ok(result.rows_affected() > 0)
204 }
205}
206
207fn bind_shared_template_fields<'q>(
208 query: sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments>,
209 template: &DcsTemplate,
210) -> DbResult<sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments>> {
211 template
212 .pid_rounding
213 .validate()
214 .map_err(DbError::InvalidPidRounding)?;
215 let versions_json = json_text("template versions", &template.versions)?;
216 Ok(query
217 .bind(template.revert_mode)
218 .bind(enum_to_text(&template.proportional_type)?)
219 .bind(enum_to_text(&template.integral_type)?)
220 .bind(enum_to_text(&template.integral_unit)?)
221 .bind(enum_to_text(&template.derivative_type)?)
222 .bind(enum_to_text(&template.derivative_unit)?)
223 .bind(enum_to_text(&template.pid_rounding.kind)?)
224 .bind(template.pid_rounding.digits)
225 .bind(&template.process_variable_suffix)
226 .bind(&template.manipulated_variable_suffix)
227 .bind(&template.setpoint_variable_suffix)
228 .bind(&template.controller_direction_suffix)
229 .bind(&template.controller_mode_suffix)
230 .bind(&template.mode_attribute_suffix)
231 .bind(&template.upper_pv_range_suffix)
232 .bind(&template.lower_pv_range_suffix)
233 .bind(&template.upper_mv_range_suffix)
234 .bind(&template.lower_mv_range_suffix)
235 .bind(&template.proportional_constant_suffix)
236 .bind(&template.integral_constant_suffix)
237 .bind(&template.derivative_constant_suffix)
238 .bind(&template.mode_manual_value)
239 .bind(&template.mode_auto_value)
240 .bind(&template.mode_attribute_program_value)
241 .bind(&template.controller_action_direct_value)
242 .bind(versions_json)
243 .bind(&template.description)
244 .bind(&template.source))
245}
246
247fn row_to_dcs_template(row: SqliteRow) -> DbResult<DcsTemplateRow> {
248 let get_enum = |column: &'static str| -> DbResult<String> {
249 row.try_get::<String, _>(column).map_err(DbError::Query)
250 };
251
252 let template = DcsTemplate {
253 name: row.try_get("name").map_err(DbError::Query)?,
254 revert_mode: row.try_get("revert_mode").map_err(DbError::Query)?,
255 proportional_type: text_to_enum("proportional_type", &get_enum("proportional_type")?)?,
256 integral_type: text_to_enum("integral_type", &get_enum("integral_type")?)?,
257 integral_unit: text_to_enum("integral_unit", &get_enum("integral_unit")?)?,
258 derivative_type: text_to_enum("derivative_type", &get_enum("derivative_type")?)?,
259 derivative_unit: text_to_enum("derivative_unit", &get_enum("derivative_unit")?)?,
260 pid_rounding: PidRounding {
261 kind: text_to_enum("pid_rounding_kind", &get_enum("pid_rounding_kind")?)?,
262 digits: row.try_get("pid_rounding_digits").map_err(DbError::Query)?,
263 },
264 process_variable_suffix: row
265 .try_get("process_variable_suffix")
266 .map_err(DbError::Query)?,
267 manipulated_variable_suffix: row
268 .try_get("manipulated_variable_suffix")
269 .map_err(DbError::Query)?,
270 setpoint_variable_suffix: row
271 .try_get("setpoint_variable_suffix")
272 .map_err(DbError::Query)?,
273 controller_direction_suffix: row
274 .try_get("controller_direction_suffix")
275 .map_err(DbError::Query)?,
276 controller_mode_suffix: row
277 .try_get("controller_mode_suffix")
278 .map_err(DbError::Query)?,
279 mode_attribute_suffix: row
280 .try_get("mode_attribute_suffix")
281 .map_err(DbError::Query)?,
282 upper_pv_range_suffix: row
283 .try_get("upper_pv_range_suffix")
284 .map_err(DbError::Query)?,
285 lower_pv_range_suffix: row
286 .try_get("lower_pv_range_suffix")
287 .map_err(DbError::Query)?,
288 upper_mv_range_suffix: row
289 .try_get("upper_mv_range_suffix")
290 .map_err(DbError::Query)?,
291 lower_mv_range_suffix: row
292 .try_get("lower_mv_range_suffix")
293 .map_err(DbError::Query)?,
294 proportional_constant_suffix: row
295 .try_get("proportional_constant_suffix")
296 .map_err(DbError::Query)?,
297 integral_constant_suffix: row
298 .try_get("integral_constant_suffix")
299 .map_err(DbError::Query)?,
300 derivative_constant_suffix: row
301 .try_get("derivative_constant_suffix")
302 .map_err(DbError::Query)?,
303 mode_manual_value: row.try_get("mode_manual_value").map_err(DbError::Query)?,
304 mode_auto_value: row.try_get("mode_auto_value").map_err(DbError::Query)?,
305 mode_attribute_program_value: row
306 .try_get("mode_attribute_program_value")
307 .map_err(DbError::Query)?,
308 controller_action_direct_value: row
309 .try_get("controller_action_direct_value")
310 .map_err(DbError::Query)?,
311 versions: {
312 let versions_json: String = row.try_get("versions_json").map_err(DbError::Query)?;
313 serde_json::from_str(&versions_json).map_err(|source| DbError::InvalidJsonShape {
314 column: "versions_json",
315 source,
316 })?
317 },
318 description: row.try_get("description").map_err(DbError::Query)?,
319 source: row.try_get("source").map_err(DbError::Query)?,
320 };
321 template
322 .pid_rounding
323 .validate()
324 .map_err(DbError::InvalidPidRounding)?;
325
326 Ok(DcsTemplateRow {
327 id: row.try_get("id").map_err(DbError::Query)?,
328 origin: text_to_enum("origin", &get_enum("origin")?)?,
329 template,
330 created_at: row.try_get("created_at").map_err(DbError::Query)?,
331 updated_at: row.try_get("updated_at").map_err(DbError::Query)?,
332 })
333}
334