1use anyhow::bail;
2use bms_dtos::TrendDataRaw;
3use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc};
4use serde::{Deserialize, Serialize, ser::SerializeStruct};
5use tokio_postgres::{Client, Config, NoTls, Row, ToStatement, types::ToSql};
6
7type Result<T> = core::result::Result<T, anyhow::Error>;
8
9pub struct AphomieDbClient {
10 db_client: Client,
11}
12
13impl AphomieDbClient {
14 pub async fn new(host: Option<&str>, port: Option<u16>) -> Result<Self> {
15 let host = if let Some(v) = host { v } else { "localhost" };
16
17 let port = if let Some(v) = port { v } else { 5432 };
18
19 let mut config = Config::new();
20 config
21 .user("dat7")
22 .password("dat7")
23 .dbname("dat7")
24 .host(host)
25 .port(port);
26
27 let (db_client, connection) = config.connect(NoTls).await?;
28
29 tokio::spawn(async move {
30 if let Err(e) = connection.await {
31 eprintln!("connection error: {}", e);
32 }
33 });
34
35 Ok(Self { db_client })
36 }
37
38 pub async fn query<T>(&self, statement: &T, params: &[&(dyn ToSql + Sync)]) -> Result<Vec<Row>>
39 where
40 T: ?Sized + ToStatement,
41 {
42 self.db_client
43 .query(statement, params)
44 .await
45 .map_err(|e| anyhow::anyhow!("failed performing query: {:?}", e))
46 }
47
48 pub async fn insert_metadata(&self, metadata: &MetadataDb) -> Result<Vec<MetadataDb>> {
49 insert_metadata(
50 &self.db_client,
51 &metadata.log_id,
52 &metadata.source,
53 &metadata.unit,
54 &metadata.alias,
55 )
56 .await
57 .map(|v| v.iter().map(|r| MetadataDb::from(r)).collect())
58 }
59
60 pub async fn get_metadata_by_source(&self, source: &str) -> Result<Vec<MetadataDb>> {
61 get_metadata_by_source(&self.db_client, source)
62 .await
63 .map(|v| v.iter().map(|r| MetadataDb::from(r)).collect())
64 }
65
66 pub async fn get_latest_trend_like_source_and_alias(
67 &self,
68 source: &str,
69 alias: &str,
70 ) -> Result<Vec<MetadataTrendDataJoinDb>> {
71 get_latest_trend_like_source_and_alias(&self.db_client, source, alias)
72 .await
73 .map(|v| v.iter().map(|r| MetadataTrendDataJoinDb::from(r)).collect())
74 }
75
76 pub async fn get_latest_trend_like_source(
77 &self,
78 source: &str,
79 ) -> Result<Vec<MetadataTrendDataJoinDb>> {
80 get_latest_trend_like_source(&self.db_client, source)
81 .await
82 .map(|v| v.iter().map(|r| MetadataTrendDataJoinDb::from(r)).collect())
83 }
84
85 pub async fn get_all_metadata(&self) -> Result<Vec<MetadataDb>> {
86 get_all_metadata(&self.db_client)
87 .await
88 .map(|v| v.iter().map(|r| MetadataDb::from(r)).collect())
89 }
90
91 pub async fn insert_trend_data(&self, trend_data: &TrendDataDb) -> Result<Vec<TrendDataDb>> {
92 insert_trend_data(
93 &self.db_client,
94 &trend_data.log_id,
95 &trend_data.timestamp,
96 &trend_data.value,
97 )
98 .await
99 .map(|v| v.iter().map(|r| TrendDataDb::from(r)).collect())
100 }
101
102 pub async fn get_trend_data_by_period(
103 &self,
104 start: &DateTime<Utc>,
105 end: &DateTime<Utc>,
106 ) -> Result<Vec<TrendDataDb>> {
107 get_trend_data_by_period(&self.db_client, start, end)
108 .await
109 .map(|v| v.iter().map(|r| TrendDataDb::from(r)).collect())
110 }
111
112 pub async fn get_trend_data_by_source_offset_limit(
113 &self,
114 source: &str,
115 offset: i64,
116 limit: i64,
117 ) -> Result<Vec<MetadataTrendDataJoinDb>> {
118 get_trend_data_by_source_offset_limit(&self.db_client, source, offset, limit)
119 .await
120 .map(|v| v.iter().map(|r| MetadataTrendDataJoinDb::from(r)).collect())
121 }
122
123 pub async fn get_metadata_join_trend_data_offset_limit(
124 &self,
125 offset: i64,
126 limit: i64,
127 ) -> Result<Vec<MetadataTrendDataJoinDb>> {
128 get_metadata_join_trend_data_offset_limit(&self.db_client, offset, limit)
129 .await
130 .map(|v| v.iter().map(|r| MetadataTrendDataJoinDb::from(r)).collect())
131 }
132
133 pub async fn get_latest_trend_data(&self) -> Result<Option<TrendDataDb>> {
134 match get_latest_trend_data(&self.db_client).await.map(|v| {
135 v.iter()
136 .map(|e| TrendDataDb::from(e))
137 .collect::<Vec<TrendDataDb>>()
138 }) {
139 Ok(v) => Ok(v.get(0).map(|e| e.clone())),
140 Err(e) => bail!(e),
141 }
142 }
143
144 pub async fn get_latest_trend_by_log_id(&self, log_id: i64) -> Result<Option<TrendDataDb>> {
145 get_latest_trend_by_log_id(&self.db_client, log_id)
146 .await
147 .map(|v| {
148 v.iter()
149 .map(|r| TrendDataDb::from(r))
150 .collect::<Vec<TrendDataDb>>()
151 })
152 .map(|e| e.get(0).map(|e| e.clone()))
153 }
154
155 pub async fn get_metadata_by_log_id(&self, log_id: i64) -> Result<Option<MetadataDb>> {
156 get_metadata_by_id(&self.db_client, log_id)
157 .await
158 .map(|v| {
159 v.iter()
160 .map(|r| MetadataDb::from(r))
161 .collect::<Vec<MetadataDb>>()
162 })
163 .map(|e| e.get(0).map(|e| e.clone()))
164 }
165
166 pub async fn get_trends_like_source_and_alias_in_dt_range_count(
167 &self,
168 source: &str,
169 alias: &str,
170 start: &DateTime<Utc>,
171 end: &DateTime<Utc>,
172 offset: i64,
173 limit: i64,
174 ) -> Result<Vec<MetadataTrendDataJoinDb>> {
175 get_trends_like_source_and_alias_count(
176 &self.db_client,
177 source,
178 alias,
179 start,
180 end,
181 offset,
182 limit,
183 )
184 .await
185 .map(|v| v.iter().map(|r| MetadataTrendDataJoinDb::from(r)).collect())
186 }
187}
188
189async fn insert_metadata(
190 client: &Client,
191 log_id: &i64,
192 source: &str,
193 unit: &str,
194 alias: &Option<String>,
195) -> Result<Vec<Row>> {
196 let stmt =
197 "INSERT INTO metadata (log_id, source, unit, alias) VALUES ($1, $2, $3, $4) RETURNING *;";
198
199 match client.query(stmt, &[log_id, &source, &unit, &alias]).await {
200 Ok(v) => Ok(v),
201 Err(e) => {
202 bail!("error querying database: {e}");
203 }
204 }
205}
206
207async fn get_metadata_by_source(client: &Client, source: &str) -> Result<Vec<Row>> {
208 let stmt = "SELECT * FROM metadata WHERE source = $1;";
209
210 match client.query(stmt, &[&source]).await {
211 Ok(v) => Ok(v),
212 Err(e) => {
213 bail!("error querying database: {e}");
214 }
215 }
216}
217
218async fn get_latest_trend_like_source_and_alias(
219 client: &Client,
220 source: &str,
221 alias: &str,
222) -> Result<Vec<Row>> {
223 let stmt = "SELECT m.log_id, m.source, m.unit, m.alias, t.timestamp, t.value FROM metadata m JOIN trend_data t on t.log_id = m.log_id WHERE m.source LIKE $1 AND m.alias = $2 ORDER BY t.timestamp DESC LIMIT 1;";
224
225 match client.query(stmt, &[&source, &alias]).await {
226 Ok(v) => Ok(v),
227 Err(e) => {
228 bail!("error querying database: {e}");
229 }
230 }
231}
232
233async fn get_latest_trend_like_source(client: &Client, source: &str) -> Result<Vec<Row>> {
234 let stmt = "SELECT m.log_id, m.source, m.unit, m.alias, t.timestamp, t.value FROM metadata m JOIN trend_data t on t.log_id = m.log_id WHERE m.source LIKE $1 ORDER BY t.timestamp DESC LIMIT 1;";
235
236 match client.query(stmt, &[&source]).await {
237 Ok(v) => Ok(v),
238 Err(e) => {
239 bail!("error querying database: {e}");
240 }
241 }
242}
243
244async fn get_metadata_by_id(client: &Client, log_id: i64) -> Result<Vec<Row>> {
245 let stmt = "SELECT * FROM metadata WHERE log_id = $1;";
246
247 match client.query(stmt, &[&log_id]).await {
248 Ok(v) => Ok(v),
249 Err(e) => {
250 bail!("error querying database: {e}");
251 }
252 }
253}
254
255async fn get_latest_trend_by_log_id(client: &Client, log_id: i64) -> Result<Vec<Row>> {
256 let stmt = "SELECT * FROM trend_data WHERE log_id = $1 ORDER BY timestamp DESC LIMIT 1;";
257
258 match client.query(stmt, &[&log_id]).await {
259 Ok(v) => Ok(v),
260 Err(e) => {
261 bail!("error querying database: {e}");
262 }
263 }
264}
265
266async fn get_all_metadata(client: &Client) -> Result<Vec<Row>> {
267 let stmt = "SELECT * FROM metadata;";
268
269 match client.query(stmt, &[]).await {
270 Ok(v) => Ok(v),
271 Err(e) => {
272 bail!("error querying database: {e}");
273 }
274 }
275}
276
277async fn get_trend_data_by_period(
278 client: &Client,
279 start: &DateTime<Utc>,
280 end: &DateTime<Utc>,
281) -> Result<Vec<Row>> {
282 let stmt = "SELECT * FROM trend_data WHERE timestamp <= $1 AND timestamp >= $2;";
283
284 match client.query(stmt, &[&end, &start]).await {
285 Ok(v) => Ok(v),
286 Err(e) => {
287 bail!("error querying database: {e}");
288 }
289 }
290}
291
292async fn get_trend_data_by_source_offset_limit(
293 client: &Client,
294 source: &str,
295 offset: i64,
296 limit: i64,
297) -> Result<Vec<Row>> {
298 let stmt = "SELECT t.log_id, t.timestamp, t.value, m.source, m.unit FROM metadata m JOIN trend_data t ON m.log_id = t.log_id WHERE source LIKE $1 ORDER BY t.log_id, t.timestamp OFFSET $2 LIMIT $3;";
299
300 match client.query(stmt, &[&source, &offset, &limit]).await {
301 Ok(v) => Ok(v),
302 Err(e) => {
303 bail!("error querying database: {e}");
304 }
305 }
306}
307
308async fn get_metadata_join_trend_data_offset_limit(
309 client: &Client,
310 offset: i64,
311 limit: i64,
312) -> Result<Vec<Row>> {
313 let stmt = "SELECT t.log_id, t.timestamp, t.value, m.source, m.unit FROM metadata m JOIN trend_data t ON m.log_id = t.log_id ORDER BY t.timestamp, t.log_id OFFSET $1 LIMIT $2;";
314
315 match client.query(stmt, &[&offset, &limit]).await {
316 Ok(v) => Ok(v),
317 Err(e) => {
318 bail!("error querying database: {e}");
319 }
320 }
321}
322
323async fn get_latest_trend_data(client: &Client) -> Result<Vec<Row>> {
324 let stmt = "SELECT * FROM trend_data ORDER BY timestamp DESC LIMIT 1";
325
326 match client.query(stmt, &[]).await {
327 Ok(v) => Ok(v),
328 Err(e) => {
329 bail!("error querying database: {e}");
330 }
331 }
332}
333
334async fn insert_trend_data(
335 client: &Client,
336 log_id: &i64,
337 timestamp: &DateTime<Utc>,
338 value: &Option<f64>,
339) -> Result<Vec<Row>> {
340 let stmt = "INSERT INTO trend_data (log_id, timestamp, value) VALUES ($1, $2, $3) RETURNING *;";
341
342 match client.query(stmt, &[log_id, ×tamp, &value]).await {
343 Ok(v) => Ok(v),
344 Err(e) => {
345 bail!("error querying database: {e}");
346 }
347 }
348}
349
350async fn get_trends_like_source_and_alias_count(
351 client: &Client,
352 source: &str,
353 alias: &str,
354 start: &DateTime<Utc>,
355 end: &DateTime<Utc>,
356 offset: i64,
357 limit: i64,
358) -> Result<Vec<Row>> {
359 let stmt = "
360SELECT m.log_id, m.source, m.unit, m.alias, t.timestamp, t.value
361FROM metadata m JOIN trend_data t on t.log_id = m.log_id
362WHERE 1=1
363 AND m.source LIKE $1
364 AND m.alias LIKE $2
365 AND t.timestamp >= $3
366 AND t.timestamp < $4
367ORDER BY t.timestamp OFFSET $5 LIMIT $6;
368";
369
370 match client
371 .query(stmt, &[&source, &alias, start, end, &offset, &limit])
372 .await
373 {
374 Ok(v) => Ok(v),
375 Err(e) => {
376 bail!("error querying database: {e}");
377 }
378 }
379}
380
381#[derive(Debug, Clone)]
382pub struct MetadataTrendDataJoinDb {
383 pub log_id: i64,
384 pub source: String,
385 pub unit: String,
386 pub timestamp: DateTime<Utc>,
387 pub value: Option<f64>,
388 pub alias: Option<String>,
389}
390
391#[derive(Debug, Clone)]
392pub struct TrendDataDb {
393 pub log_id: i64,
394 pub timestamp: DateTime<Utc>,
395 pub value: Option<f64>,
396}
397
398impl Serialize for MetadataTrendDataJoinDb {
399 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
400 where
401 S: serde::Serializer,
402 {
403 let mut s = serializer.serialize_struct("TrendDataDb", 3)?;
404 s.serialize_field("log_id", &self.log_id)?;
405 s.serialize_field("timestamp", &self.timestamp.to_rfc3339())?;
406 s.serialize_field("value", &self.value)?;
407 s.serialize_field("source", &self.source)?;
408 s.serialize_field("unit", &self.unit)?;
409 s.serialize_field("alias", &self.alias)?;
410 s.end()
411 }
412}
413
414impl Serialize for TrendDataDb {
415 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
416 where
417 S: serde::Serializer,
418 {
419 let mut s = serializer.serialize_struct("TrendDataDb", 3)?;
420 s.serialize_field("log_id", &self.log_id)?;
421 s.serialize_field("timestamp", &self.timestamp.to_rfc3339())?;
422 s.serialize_field("value", &self.value)?;
423 s.end()
424 }
425}
426
427#[derive(Clone, Serialize, Deserialize, Debug)]
428pub struct MetadataDb {
429 pub log_id: i64,
430 pub source: String,
431 pub unit: String,
432 pub alias: Option<String>,
433}
434
435impl From<&Row> for MetadataTrendDataJoinDb {
436 fn from(value: &Row) -> Self {
437 let log_id: i64 = value.get("log_id");
438 let source: String = value.get("source");
439 let unit: String = value.get("unit");
440 let timestamp: DateTime<Utc> = value.get("timestamp");
441 let alias: Option<String> = value.get("alias");
442 let value: Option<f64> = value.get("value");
443
444 MetadataTrendDataJoinDb {
445 value,
446 log_id,
447 source,
448 unit,
449 timestamp,
450 alias,
451 }
452 }
453}
454
455impl From<&Row> for MetadataDb {
456 fn from(value: &Row) -> MetadataDb {
457 let log_id: i64 = value.get("log_id");
458 let source: String = value.get("source");
459 let unit: String = value.get("unit");
460 let alias: Option<String> = value.get("alias");
461
462 MetadataDb {
463 log_id,
464 source,
465 unit,
466 alias,
467 }
468 }
469}
470
471impl TryFrom<&TrendDataRaw> for TrendDataDb {
472 type Error = anyhow::Error;
473
474 fn try_from(value: &TrendDataRaw) -> core::result::Result<Self, Self::Error> {
475 let naive_timestamp = NaiveDateTime::parse_from_str(&value.timestamp, "%Y-%m-%d %H:%M:%S");
476 if let Err(e) = naive_timestamp {
477 let timestamp = &value.timestamp;
478 bail!("error parsing {timestamp}: {e}");
479 }
480 let mut naive_timestamp = naive_timestamp.unwrap();
481 match value.timestamp_tzinfo {
482 248 => {
483 naive_timestamp = naive_timestamp - TimeDelta::hours(1);
485 }
486 241 => {
487 naive_timestamp = naive_timestamp - TimeDelta::hours(2);
489 }
490 _ => {
491 let timezone = value.timestamp_tzinfo;
492 bail!("did not recognize timestamp timezone {timezone}");
493 }
494 }
495
496 let utc_time: DateTime<Utc> = naive_timestamp.and_local_timezone(Utc).unwrap();
497
498 Ok(TrendDataDb {
499 log_id: value.externallogid,
500 timestamp: utc_time,
501 value: value.value,
502 })
503 }
504}
505
506impl From<&Row> for TrendDataDb {
507 fn from(row: &Row) -> TrendDataDb {
508 let log_id: i64 = row.get("log_id");
509 let timestamp: DateTime<Utc> = row.get("timestamp");
510 let value: Option<f64> = row.get("value");
511
512 TrendDataDb {
513 log_id,
514 timestamp,
515 value,
516 }
517 }
518}