1use time::OffsetDateTime;
35
36pub const MAX_QUERY_LIMIT: u32 = 1_000_000;
39
40#[derive(Debug, Default, Clone)]
74pub struct ReadingQuery {
75 pub device_id: Option<String>,
77 pub since: Option<OffsetDateTime>,
79 pub until: Option<OffsetDateTime>,
81 pub limit: Option<u32>,
83 pub offset: Option<u32>,
85 pub newest_first: bool,
87}
88
89impl ReadingQuery {
90 pub fn new() -> Self {
98 Self {
99 newest_first: true,
100 ..Default::default()
101 }
102 }
103
104 pub fn device(mut self, device_id: &str) -> Self {
108 self.device_id = Some(device_id.to_string());
109 self
110 }
111
112 pub fn since(mut self, time: OffsetDateTime) -> Self {
116 self.since = Some(time);
117 self
118 }
119
120 pub fn until(mut self, time: OffsetDateTime) -> Self {
124 self.until = Some(time);
125 self
126 }
127
128 pub fn limit(mut self, limit: u32) -> Self {
133 self.limit = Some(limit.min(MAX_QUERY_LIMIT));
134 self
135 }
136
137 pub fn offset(mut self, offset: u32) -> Self {
144 self.offset = Some(offset.min(MAX_QUERY_LIMIT));
145 self
146 }
147
148 pub fn oldest_first(mut self) -> Self {
153 self.newest_first = false;
154 self
155 }
156
157 pub(crate) fn build_where(&self) -> (String, Vec<Box<dyn rusqlite::ToSql>>) {
159 let mut conditions = Vec::new();
160 let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
161
162 if let Some(ref device_id) = self.device_id {
163 conditions.push("device_id = ?");
164 params.push(Box::new(device_id.clone()));
165 }
166
167 if let Some(since) = self.since {
168 conditions.push("captured_at >= ?");
169 params.push(Box::new(since.unix_timestamp()));
170 }
171
172 if let Some(until) = self.until {
173 conditions.push("captured_at <= ?");
174 params.push(Box::new(until.unix_timestamp()));
175 }
176
177 let where_clause = if conditions.is_empty() {
178 String::new()
179 } else {
180 format!("WHERE {}", conditions.join(" AND "))
181 };
182
183 (where_clause, params)
184 }
185
186 pub(crate) fn build_sql(&self) -> String {
188 let (where_clause, _) = self.build_where();
189 let order = if self.newest_first { "DESC" } else { "ASC" };
190
191 let mut sql = format!(
192 "SELECT id, device_id, captured_at, co2, temperature, pressure, humidity, \
193 battery, status, radon, radiation_rate, radiation_total, \
194 radon_avg_24h, radon_avg_7d, radon_avg_30d \
195 FROM readings {} ORDER BY captured_at {}",
196 where_clause, order
197 );
198
199 if let Some(limit) = self.limit {
200 sql.push_str(&format!(" LIMIT {}", limit));
201 }
202
203 if let Some(offset) = self.offset {
204 sql.push_str(&format!(" OFFSET {}", offset));
205 }
206
207 sql
208 }
209}
210
211#[derive(Debug, Default, Clone)]
241pub struct HistoryQuery {
242 pub device_id: Option<String>,
244 pub since: Option<OffsetDateTime>,
246 pub until: Option<OffsetDateTime>,
248 pub limit: Option<u32>,
250 pub offset: Option<u32>,
252 pub newest_first: bool,
254}
255
256impl HistoryQuery {
257 pub fn new() -> Self {
265 Self {
266 newest_first: true,
267 ..Default::default()
268 }
269 }
270
271 pub fn device(mut self, device_id: &str) -> Self {
275 self.device_id = Some(device_id.to_string());
276 self
277 }
278
279 pub fn since(mut self, time: OffsetDateTime) -> Self {
283 self.since = Some(time);
284 self
285 }
286
287 pub fn until(mut self, time: OffsetDateTime) -> Self {
291 self.until = Some(time);
292 self
293 }
294
295 pub fn limit(mut self, limit: u32) -> Self {
300 self.limit = Some(limit.min(MAX_QUERY_LIMIT));
301 self
302 }
303
304 pub fn offset(mut self, offset: u32) -> Self {
311 self.offset = Some(offset.min(MAX_QUERY_LIMIT));
312 self
313 }
314
315 pub fn oldest_first(mut self) -> Self {
320 self.newest_first = false;
321 self
322 }
323
324 pub(crate) fn build_where(&self) -> (String, Vec<Box<dyn rusqlite::ToSql>>) {
326 let mut conditions = Vec::new();
327 let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
328
329 if let Some(ref device_id) = self.device_id {
330 conditions.push("device_id = ?");
331 params.push(Box::new(device_id.clone()));
332 }
333
334 if let Some(since) = self.since {
335 conditions.push("timestamp >= ?");
336 params.push(Box::new(since.unix_timestamp()));
337 }
338
339 if let Some(until) = self.until {
340 conditions.push("timestamp <= ?");
341 params.push(Box::new(until.unix_timestamp()));
342 }
343
344 let where_clause = if conditions.is_empty() {
345 String::new()
346 } else {
347 format!("WHERE {}", conditions.join(" AND "))
348 };
349
350 (where_clause, params)
351 }
352
353 pub(crate) fn build_sql_with_select(&self, select: &str) -> String {
355 let (where_clause, _) = self.build_where();
356 let order = if self.newest_first { "DESC" } else { "ASC" };
357
358 let mut sql = format!("{select} {where_clause} ORDER BY timestamp {order}");
359
360 if let Some(limit) = self.limit {
361 sql.push_str(&format!(" LIMIT {}", limit));
362 }
363
364 if let Some(offset) = self.offset {
365 sql.push_str(&format!(" OFFSET {}", offset));
366 }
367
368 sql
369 }
370
371 pub(crate) fn build_sql(&self) -> String {
373 self.build_sql_with_select(
374 "SELECT id, device_id, timestamp, synced_at, co2, temperature, pressure, \
375 humidity, radon, radiation_rate, radiation_total FROM history",
376 )
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use time::macros::datetime;
384
385 #[test]
388 fn test_reading_query_new_defaults() {
389 let query = ReadingQuery::new();
390 assert!(query.device_id.is_none());
391 assert!(query.since.is_none());
392 assert!(query.until.is_none());
393 assert!(query.limit.is_none());
394 assert!(query.offset.is_none());
395 assert!(query.newest_first);
396 }
397
398 #[test]
399 fn test_reading_query_default_is_different_from_new() {
400 let default_query = ReadingQuery::default();
401 let new_query = ReadingQuery::new();
402
403 assert!(!default_query.newest_first);
405 assert!(new_query.newest_first);
406 }
407
408 #[test]
409 fn test_reading_query_device_filter() {
410 let query = ReadingQuery::new().device("test-device-123");
411 assert_eq!(query.device_id, Some("test-device-123".to_string()));
412 }
413
414 #[test]
415 fn test_reading_query_since_filter() {
416 let time = datetime!(2024-01-15 10:30:00 UTC);
417 let query = ReadingQuery::new().since(time);
418 assert_eq!(query.since, Some(time));
419 }
420
421 #[test]
422 fn test_reading_query_until_filter() {
423 let time = datetime!(2024-01-15 18:30:00 UTC);
424 let query = ReadingQuery::new().until(time);
425 assert_eq!(query.until, Some(time));
426 }
427
428 #[test]
429 fn test_reading_query_limit() {
430 let query = ReadingQuery::new().limit(100);
431 assert_eq!(query.limit, Some(100));
432 }
433
434 #[test]
435 fn test_reading_query_offset() {
436 let query = ReadingQuery::new().offset(50);
437 assert_eq!(query.offset, Some(50));
438 }
439
440 #[test]
441 fn test_reading_query_oldest_first() {
442 let query = ReadingQuery::new().oldest_first();
443 assert!(!query.newest_first);
444 }
445
446 #[test]
447 fn test_reading_query_chaining() {
448 let since = datetime!(2024-01-01 00:00:00 UTC);
449 let until = datetime!(2024-12-31 23:59:59 UTC);
450
451 let query = ReadingQuery::new()
452 .device("my-device")
453 .since(since)
454 .until(until)
455 .limit(10)
456 .offset(5)
457 .oldest_first();
458
459 assert_eq!(query.device_id, Some("my-device".to_string()));
460 assert_eq!(query.since, Some(since));
461 assert_eq!(query.until, Some(until));
462 assert_eq!(query.limit, Some(10));
463 assert_eq!(query.offset, Some(5));
464 assert!(!query.newest_first);
465 }
466
467 #[test]
468 fn test_reading_query_build_where_empty() {
469 let query = ReadingQuery::new();
470 let (where_clause, params) = query.build_where();
471 assert_eq!(where_clause, "");
472 assert!(params.is_empty());
473 }
474
475 #[test]
476 fn test_reading_query_build_where_device_only() {
477 let query = ReadingQuery::new().device("test-device");
478 let (where_clause, params) = query.build_where();
479 assert_eq!(where_clause, "WHERE device_id = ?");
480 assert_eq!(params.len(), 1);
481 }
482
483 #[test]
484 fn test_reading_query_build_where_time_range() {
485 let since = datetime!(2024-01-01 00:00:00 UTC);
486 let until = datetime!(2024-12-31 23:59:59 UTC);
487
488 let query = ReadingQuery::new().since(since).until(until);
489 let (where_clause, params) = query.build_where();
490
491 assert_eq!(where_clause, "WHERE captured_at >= ? AND captured_at <= ?");
492 assert_eq!(params.len(), 2);
493 }
494
495 #[test]
496 fn test_reading_query_build_where_all_filters() {
497 let since = datetime!(2024-01-01 00:00:00 UTC);
498 let until = datetime!(2024-12-31 23:59:59 UTC);
499
500 let query = ReadingQuery::new()
501 .device("device-1")
502 .since(since)
503 .until(until);
504 let (where_clause, params) = query.build_where();
505
506 assert!(where_clause.contains("device_id = ?"));
507 assert!(where_clause.contains("captured_at >= ?"));
508 assert!(where_clause.contains("captured_at <= ?"));
509 assert_eq!(params.len(), 3);
510 }
511
512 #[test]
513 fn test_reading_query_build_sql_basic() {
514 let query = ReadingQuery::new();
515 let sql = query.build_sql();
516
517 assert!(sql.contains("SELECT"));
518 assert!(sql.contains("FROM readings"));
519 assert!(sql.contains("ORDER BY captured_at DESC"));
520 assert!(!sql.contains("WHERE"));
521 assert!(!sql.contains("LIMIT"));
522 assert!(!sql.contains("OFFSET"));
523 }
524
525 #[test]
526 fn test_reading_query_build_sql_with_limit() {
527 let query = ReadingQuery::new().limit(50);
528 let sql = query.build_sql();
529
530 assert!(sql.contains("LIMIT 50"));
531 }
532
533 #[test]
534 fn test_reading_query_build_sql_with_offset() {
535 let query = ReadingQuery::new().offset(25);
536 let sql = query.build_sql();
537
538 assert!(sql.contains("OFFSET 25"));
539 }
540
541 #[test]
542 fn test_reading_query_build_sql_oldest_first() {
543 let query = ReadingQuery::new().oldest_first();
544 let sql = query.build_sql();
545
546 assert!(sql.contains("ORDER BY captured_at ASC"));
547 }
548
549 #[test]
550 fn test_reading_query_build_sql_complete() {
551 let since = datetime!(2024-06-01 00:00:00 UTC);
552 let query = ReadingQuery::new()
553 .device("my-sensor")
554 .since(since)
555 .limit(100)
556 .offset(10)
557 .oldest_first();
558
559 let sql = query.build_sql();
560
561 assert!(sql.contains("WHERE"));
562 assert!(sql.contains("device_id = ?"));
563 assert!(sql.contains("captured_at >= ?"));
564 assert!(sql.contains("ORDER BY captured_at ASC"));
565 assert!(sql.contains("LIMIT 100"));
566 assert!(sql.contains("OFFSET 10"));
567 }
568
569 #[test]
570 fn test_reading_query_build_sql_selects_all_columns() {
571 let query = ReadingQuery::new();
572 let sql = query.build_sql();
573
574 assert!(sql.contains("id"));
575 assert!(sql.contains("device_id"));
576 assert!(sql.contains("captured_at"));
577 assert!(sql.contains("co2"));
578 assert!(sql.contains("temperature"));
579 assert!(sql.contains("pressure"));
580 assert!(sql.contains("humidity"));
581 assert!(sql.contains("battery"));
582 assert!(sql.contains("status"));
583 assert!(sql.contains("radon"));
584 assert!(sql.contains("radiation_rate"));
585 assert!(sql.contains("radiation_total"));
586 assert!(sql.contains("radon_avg_24h"));
587 assert!(sql.contains("radon_avg_7d"));
588 assert!(sql.contains("radon_avg_30d"));
589 }
590
591 #[test]
594 fn test_history_query_new_defaults() {
595 let query = HistoryQuery::new();
596 assert!(query.device_id.is_none());
597 assert!(query.since.is_none());
598 assert!(query.until.is_none());
599 assert!(query.limit.is_none());
600 assert!(query.offset.is_none());
601 assert!(query.newest_first);
602 }
603
604 #[test]
605 fn test_history_query_default_is_different_from_new() {
606 let default_query = HistoryQuery::default();
607 let new_query = HistoryQuery::new();
608
609 assert!(!default_query.newest_first);
611 assert!(new_query.newest_first);
612 }
613
614 #[test]
615 fn test_history_query_device_filter() {
616 let query = HistoryQuery::new().device("aranet4-abc123");
617 assert_eq!(query.device_id, Some("aranet4-abc123".to_string()));
618 }
619
620 #[test]
621 fn test_history_query_since_filter() {
622 let time = datetime!(2024-03-15 08:00:00 UTC);
623 let query = HistoryQuery::new().since(time);
624 assert_eq!(query.since, Some(time));
625 }
626
627 #[test]
628 fn test_history_query_until_filter() {
629 let time = datetime!(2024-03-15 20:00:00 UTC);
630 let query = HistoryQuery::new().until(time);
631 assert_eq!(query.until, Some(time));
632 }
633
634 #[test]
635 fn test_history_query_limit() {
636 let query = HistoryQuery::new().limit(500);
637 assert_eq!(query.limit, Some(500));
638 }
639
640 #[test]
641 fn test_history_query_offset() {
642 let query = HistoryQuery::new().offset(200);
643 assert_eq!(query.offset, Some(200));
644 }
645
646 #[test]
647 fn test_history_query_oldest_first() {
648 let query = HistoryQuery::new().oldest_first();
649 assert!(!query.newest_first);
650 }
651
652 #[test]
653 fn test_history_query_chaining() {
654 let since = datetime!(2024-01-01 00:00:00 UTC);
655 let until = datetime!(2024-06-30 23:59:59 UTC);
656
657 let query = HistoryQuery::new()
658 .device("sensor-xyz")
659 .since(since)
660 .until(until)
661 .limit(1000)
662 .offset(100)
663 .oldest_first();
664
665 assert_eq!(query.device_id, Some("sensor-xyz".to_string()));
666 assert_eq!(query.since, Some(since));
667 assert_eq!(query.until, Some(until));
668 assert_eq!(query.limit, Some(1000));
669 assert_eq!(query.offset, Some(100));
670 assert!(!query.newest_first);
671 }
672
673 #[test]
674 fn test_history_query_clone() {
675 let query = HistoryQuery::new().device("device-1").limit(50);
676 let cloned = query.clone();
677
678 assert_eq!(cloned.device_id, query.device_id);
679 assert_eq!(cloned.limit, query.limit);
680 }
681
682 #[test]
683 fn test_reading_query_clone() {
684 let query = ReadingQuery::new().device("device-1").limit(50);
685 let cloned = query.clone();
686
687 assert_eq!(cloned.device_id, query.device_id);
688 assert_eq!(cloned.limit, query.limit);
689 }
690
691 #[test]
692 fn test_reading_query_debug() {
693 let query = ReadingQuery::new().device("test");
694 let debug_str = format!("{:?}", query);
695 assert!(debug_str.contains("ReadingQuery"));
696 assert!(debug_str.contains("test"));
697 }
698
699 #[test]
700 fn test_history_query_debug() {
701 let query = HistoryQuery::new().device("test");
702 let debug_str = format!("{:?}", query);
703 assert!(debug_str.contains("HistoryQuery"));
704 assert!(debug_str.contains("test"));
705 }
706
707 #[test]
708 fn test_reading_query_limit_zero() {
709 let query = ReadingQuery::new().limit(0);
710 let sql = query.build_sql();
711 assert!(sql.contains("LIMIT 0"));
712 }
713
714 #[test]
715 fn test_reading_query_large_pagination() {
716 let query = ReadingQuery::new().limit(u32::MAX).offset(u32::MAX);
718 let sql = query.build_sql();
719 assert!(sql.contains(&format!("LIMIT {}", MAX_QUERY_LIMIT)));
720 assert!(sql.contains(&format!("OFFSET {}", MAX_QUERY_LIMIT)));
721 }
722}