hamelin_lib 0.19.0

Core library for Hamelin query language
Documentation
use std::sync::Arc;

use chrono::{DateTime, SecondsFormat, Utc};

use crate::tree::ast::expression::{Expression, NullLiteral};
use crate::tree::ast::ops::BinaryOp;
use crate::tree::builder::{call, null, BinaryOperatorBuilder, ExpressionBuilder};

/// A time range with optional start and end bounds.
///
/// Used for resolving dynamic time ranges in queries (e.g., `[-1h..]`).
/// Converts to an AST [`Expression`] via [`From`], producing a range expression
/// like `ts('2024-01-01T00:00:00.000Z')..ts('2024-01-02T00:00:00.000Z')`.
#[derive(Debug, Clone, Default)]
pub struct TimeRange {
    pub start: Option<DateTime<Utc>>,
    pub end: Option<DateTime<Utc>>,
    /// Whether the end bound is inclusive (`..=`). Defaults to exclusive (`..`).
    pub end_inclusive: bool,
}

impl From<TimeRange> for Expression {
    fn from(range: TimeRange) -> Self {
        let ts_call =
            |dt: DateTime<Utc>| call("ts").arg(dt.to_rfc3339_opts(SecondsFormat::Millis, true));
        let range_op = if range.end_inclusive {
            BinaryOp::RangeInclusive
        } else {
            BinaryOp::Range
        };
        match (range.start, range.end) {
            (Some(start), Some(end)) => {
                BinaryOperatorBuilder::new(range_op, ts_call(start), ts_call(end)).build()
            }
            (Some(start), None) => {
                BinaryOperatorBuilder::new(range_op, ts_call(start), null()).build()
            }
            (None, Some(end)) => BinaryOperatorBuilder::new(range_op, null(), ts_call(end)).build(),
            (None, None) => Expression::from_kind(NullLiteral),
        }
    }
}

impl From<TimeRange> for Arc<Expression> {
    fn from(range: TimeRange) -> Self {
        Arc::new(Expression::from(range))
    }
}

#[cfg(test)]
mod tests {
    use chrono::{TimeZone, Utc};

    use super::*;
    use crate::tree::ast::ParseWithErrors;

    fn timestamp(hour: u32) -> DateTime<Utc> {
        Utc.with_ymd_and_hms(2024, 1, 1, hour, 0, 0)
            .single()
            .expect("valid test timestamp")
    }

    fn assert_expression(range: TimeRange, expected: &str) {
        let actual = Expression::from(range);
        let expected = Expression::parse_result(expected).expect("valid expected expression");
        assert_eq!(actual, expected);
    }

    #[test]
    fn exclusive_range_uses_exclusive_operator_by_default() {
        assert_expression(
            TimeRange {
                start: Some(timestamp(1)),
                end: Some(timestamp(2)),
                ..Default::default()
            },
            "ts('2024-01-01T01:00:00.000Z')..ts('2024-01-01T02:00:00.000Z')",
        );
    }

    #[test]
    fn inclusive_range_uses_inclusive_operator() {
        assert_expression(
            TimeRange {
                start: Some(timestamp(1)),
                end: Some(timestamp(2)),
                end_inclusive: true,
            },
            "ts('2024-01-01T01:00:00.000Z')..=ts('2024-01-01T02:00:00.000Z')",
        );
    }

    #[test]
    fn zero_width_inclusive_range_is_preserved() {
        assert_expression(
            TimeRange {
                start: Some(timestamp(1)),
                end: Some(timestamp(1)),
                end_inclusive: true,
            },
            "ts('2024-01-01T01:00:00.000Z')..=ts('2024-01-01T01:00:00.000Z')",
        );
    }
}