use time::OffsetDateTime;
pub const MAX_QUERY_LIMIT: u32 = 1_000_000;
#[derive(Debug, Default, Clone)]
pub struct ReadingQuery {
pub device_id: Option<String>,
pub since: Option<OffsetDateTime>,
pub until: Option<OffsetDateTime>,
pub limit: Option<u32>,
pub offset: Option<u32>,
pub newest_first: bool,
}
impl ReadingQuery {
pub fn new() -> Self {
Self {
newest_first: true,
..Default::default()
}
}
pub fn device(mut self, device_id: &str) -> Self {
self.device_id = Some(device_id.to_string());
self
}
pub fn since(mut self, time: OffsetDateTime) -> Self {
self.since = Some(time);
self
}
pub fn until(mut self, time: OffsetDateTime) -> Self {
self.until = Some(time);
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit.min(MAX_QUERY_LIMIT));
self
}
pub fn offset(mut self, offset: u32) -> Self {
self.offset = Some(offset);
self
}
pub fn oldest_first(mut self) -> Self {
self.newest_first = false;
self
}
pub(crate) fn build_where(&self) -> (String, Vec<Box<dyn rusqlite::ToSql>>) {
let mut conditions = Vec::new();
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
if let Some(ref device_id) = self.device_id {
conditions.push("device_id = ?");
params.push(Box::new(device_id.clone()));
}
if let Some(since) = self.since {
conditions.push("captured_at >= ?");
params.push(Box::new(since.unix_timestamp()));
}
if let Some(until) = self.until {
conditions.push("captured_at <= ?");
params.push(Box::new(until.unix_timestamp()));
}
let where_clause = if conditions.is_empty() {
String::new()
} else {
format!("WHERE {}", conditions.join(" AND "))
};
(where_clause, params)
}
pub(crate) fn build_sql(&self) -> String {
let (where_clause, _) = self.build_where();
let order = if self.newest_first { "DESC" } else { "ASC" };
let mut sql = format!(
"SELECT id, device_id, captured_at, co2, temperature, pressure, humidity, \
battery, status, radon, radiation_rate, radiation_total \
FROM readings {} ORDER BY captured_at {}",
where_clause, order
);
if let Some(limit) = self.limit {
sql.push_str(&format!(" LIMIT {}", limit));
}
if let Some(offset) = self.offset {
sql.push_str(&format!(" OFFSET {}", offset));
}
sql
}
}
#[derive(Debug, Default, Clone)]
pub struct HistoryQuery {
pub device_id: Option<String>,
pub since: Option<OffsetDateTime>,
pub until: Option<OffsetDateTime>,
pub limit: Option<u32>,
pub offset: Option<u32>,
pub newest_first: bool,
}
impl HistoryQuery {
pub fn new() -> Self {
Self {
newest_first: true,
..Default::default()
}
}
pub fn device(mut self, device_id: &str) -> Self {
self.device_id = Some(device_id.to_string());
self
}
pub fn since(mut self, time: OffsetDateTime) -> Self {
self.since = Some(time);
self
}
pub fn until(mut self, time: OffsetDateTime) -> Self {
self.until = Some(time);
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit.min(MAX_QUERY_LIMIT));
self
}
pub fn offset(mut self, offset: u32) -> Self {
self.offset = Some(offset);
self
}
pub fn oldest_first(mut self) -> Self {
self.newest_first = false;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use time::macros::datetime;
#[test]
fn test_reading_query_new_defaults() {
let query = ReadingQuery::new();
assert!(query.device_id.is_none());
assert!(query.since.is_none());
assert!(query.until.is_none());
assert!(query.limit.is_none());
assert!(query.offset.is_none());
assert!(query.newest_first);
}
#[test]
fn test_reading_query_default_is_different_from_new() {
let default_query = ReadingQuery::default();
let new_query = ReadingQuery::new();
assert!(!default_query.newest_first);
assert!(new_query.newest_first);
}
#[test]
fn test_reading_query_device_filter() {
let query = ReadingQuery::new().device("test-device-123");
assert_eq!(query.device_id, Some("test-device-123".to_string()));
}
#[test]
fn test_reading_query_since_filter() {
let time = datetime!(2024-01-15 10:30:00 UTC);
let query = ReadingQuery::new().since(time);
assert_eq!(query.since, Some(time));
}
#[test]
fn test_reading_query_until_filter() {
let time = datetime!(2024-01-15 18:30:00 UTC);
let query = ReadingQuery::new().until(time);
assert_eq!(query.until, Some(time));
}
#[test]
fn test_reading_query_limit() {
let query = ReadingQuery::new().limit(100);
assert_eq!(query.limit, Some(100));
}
#[test]
fn test_reading_query_offset() {
let query = ReadingQuery::new().offset(50);
assert_eq!(query.offset, Some(50));
}
#[test]
fn test_reading_query_oldest_first() {
let query = ReadingQuery::new().oldest_first();
assert!(!query.newest_first);
}
#[test]
fn test_reading_query_chaining() {
let since = datetime!(2024-01-01 00:00:00 UTC);
let until = datetime!(2024-12-31 23:59:59 UTC);
let query = ReadingQuery::new()
.device("my-device")
.since(since)
.until(until)
.limit(10)
.offset(5)
.oldest_first();
assert_eq!(query.device_id, Some("my-device".to_string()));
assert_eq!(query.since, Some(since));
assert_eq!(query.until, Some(until));
assert_eq!(query.limit, Some(10));
assert_eq!(query.offset, Some(5));
assert!(!query.newest_first);
}
#[test]
fn test_reading_query_build_where_empty() {
let query = ReadingQuery::new();
let (where_clause, params) = query.build_where();
assert_eq!(where_clause, "");
assert!(params.is_empty());
}
#[test]
fn test_reading_query_build_where_device_only() {
let query = ReadingQuery::new().device("test-device");
let (where_clause, params) = query.build_where();
assert_eq!(where_clause, "WHERE device_id = ?");
assert_eq!(params.len(), 1);
}
#[test]
fn test_reading_query_build_where_time_range() {
let since = datetime!(2024-01-01 00:00:00 UTC);
let until = datetime!(2024-12-31 23:59:59 UTC);
let query = ReadingQuery::new().since(since).until(until);
let (where_clause, params) = query.build_where();
assert_eq!(where_clause, "WHERE captured_at >= ? AND captured_at <= ?");
assert_eq!(params.len(), 2);
}
#[test]
fn test_reading_query_build_where_all_filters() {
let since = datetime!(2024-01-01 00:00:00 UTC);
let until = datetime!(2024-12-31 23:59:59 UTC);
let query = ReadingQuery::new()
.device("device-1")
.since(since)
.until(until);
let (where_clause, params) = query.build_where();
assert!(where_clause.contains("device_id = ?"));
assert!(where_clause.contains("captured_at >= ?"));
assert!(where_clause.contains("captured_at <= ?"));
assert_eq!(params.len(), 3);
}
#[test]
fn test_reading_query_build_sql_basic() {
let query = ReadingQuery::new();
let sql = query.build_sql();
assert!(sql.contains("SELECT"));
assert!(sql.contains("FROM readings"));
assert!(sql.contains("ORDER BY captured_at DESC"));
assert!(!sql.contains("WHERE"));
assert!(!sql.contains("LIMIT"));
assert!(!sql.contains("OFFSET"));
}
#[test]
fn test_reading_query_build_sql_with_limit() {
let query = ReadingQuery::new().limit(50);
let sql = query.build_sql();
assert!(sql.contains("LIMIT 50"));
}
#[test]
fn test_reading_query_build_sql_with_offset() {
let query = ReadingQuery::new().offset(25);
let sql = query.build_sql();
assert!(sql.contains("OFFSET 25"));
}
#[test]
fn test_reading_query_build_sql_oldest_first() {
let query = ReadingQuery::new().oldest_first();
let sql = query.build_sql();
assert!(sql.contains("ORDER BY captured_at ASC"));
}
#[test]
fn test_reading_query_build_sql_complete() {
let since = datetime!(2024-06-01 00:00:00 UTC);
let query = ReadingQuery::new()
.device("my-sensor")
.since(since)
.limit(100)
.offset(10)
.oldest_first();
let sql = query.build_sql();
assert!(sql.contains("WHERE"));
assert!(sql.contains("device_id = ?"));
assert!(sql.contains("captured_at >= ?"));
assert!(sql.contains("ORDER BY captured_at ASC"));
assert!(sql.contains("LIMIT 100"));
assert!(sql.contains("OFFSET 10"));
}
#[test]
fn test_reading_query_build_sql_selects_all_columns() {
let query = ReadingQuery::new();
let sql = query.build_sql();
assert!(sql.contains("id"));
assert!(sql.contains("device_id"));
assert!(sql.contains("captured_at"));
assert!(sql.contains("co2"));
assert!(sql.contains("temperature"));
assert!(sql.contains("pressure"));
assert!(sql.contains("humidity"));
assert!(sql.contains("battery"));
assert!(sql.contains("status"));
assert!(sql.contains("radon"));
assert!(sql.contains("radiation_rate"));
assert!(sql.contains("radiation_total"));
}
#[test]
fn test_history_query_new_defaults() {
let query = HistoryQuery::new();
assert!(query.device_id.is_none());
assert!(query.since.is_none());
assert!(query.until.is_none());
assert!(query.limit.is_none());
assert!(query.offset.is_none());
assert!(query.newest_first);
}
#[test]
fn test_history_query_default_is_different_from_new() {
let default_query = HistoryQuery::default();
let new_query = HistoryQuery::new();
assert!(!default_query.newest_first);
assert!(new_query.newest_first);
}
#[test]
fn test_history_query_device_filter() {
let query = HistoryQuery::new().device("aranet4-abc123");
assert_eq!(query.device_id, Some("aranet4-abc123".to_string()));
}
#[test]
fn test_history_query_since_filter() {
let time = datetime!(2024-03-15 08:00:00 UTC);
let query = HistoryQuery::new().since(time);
assert_eq!(query.since, Some(time));
}
#[test]
fn test_history_query_until_filter() {
let time = datetime!(2024-03-15 20:00:00 UTC);
let query = HistoryQuery::new().until(time);
assert_eq!(query.until, Some(time));
}
#[test]
fn test_history_query_limit() {
let query = HistoryQuery::new().limit(500);
assert_eq!(query.limit, Some(500));
}
#[test]
fn test_history_query_offset() {
let query = HistoryQuery::new().offset(200);
assert_eq!(query.offset, Some(200));
}
#[test]
fn test_history_query_oldest_first() {
let query = HistoryQuery::new().oldest_first();
assert!(!query.newest_first);
}
#[test]
fn test_history_query_chaining() {
let since = datetime!(2024-01-01 00:00:00 UTC);
let until = datetime!(2024-06-30 23:59:59 UTC);
let query = HistoryQuery::new()
.device("sensor-xyz")
.since(since)
.until(until)
.limit(1000)
.offset(100)
.oldest_first();
assert_eq!(query.device_id, Some("sensor-xyz".to_string()));
assert_eq!(query.since, Some(since));
assert_eq!(query.until, Some(until));
assert_eq!(query.limit, Some(1000));
assert_eq!(query.offset, Some(100));
assert!(!query.newest_first);
}
#[test]
fn test_history_query_clone() {
let query = HistoryQuery::new().device("device-1").limit(50);
let cloned = query.clone();
assert_eq!(cloned.device_id, query.device_id);
assert_eq!(cloned.limit, query.limit);
}
#[test]
fn test_reading_query_clone() {
let query = ReadingQuery::new().device("device-1").limit(50);
let cloned = query.clone();
assert_eq!(cloned.device_id, query.device_id);
assert_eq!(cloned.limit, query.limit);
}
#[test]
fn test_reading_query_debug() {
let query = ReadingQuery::new().device("test");
let debug_str = format!("{:?}", query);
assert!(debug_str.contains("ReadingQuery"));
assert!(debug_str.contains("test"));
}
#[test]
fn test_history_query_debug() {
let query = HistoryQuery::new().device("test");
let debug_str = format!("{:?}", query);
assert!(debug_str.contains("HistoryQuery"));
assert!(debug_str.contains("test"));
}
#[test]
fn test_reading_query_limit_zero() {
let query = ReadingQuery::new().limit(0);
let sql = query.build_sql();
assert!(sql.contains("LIMIT 0"));
}
#[test]
fn test_reading_query_large_pagination() {
let query = ReadingQuery::new().limit(u32::MAX).offset(u32::MAX);
let sql = query.build_sql();
assert!(sql.contains(&format!("LIMIT {}", MAX_QUERY_LIMIT)));
assert!(sql.contains(&format!("OFFSET {}", u32::MAX)));
}
}