hamelin_eval 0.11.1

Expression evaluation for Hamelin query language
Documentation
//! Eval implementations for datetime functions

use hamelin_lib::func::defs::{
    AtTimezone, Day, DayOfWeek, FromUnixtimeMicros, FromUnixtimeMillis, FromUnixtimeNanos,
    FromUnixtimeSeconds, Hour, Minute, Month, Now, Second, ToMillis, ToUnixtime, Today, Tomorrow,
    Ts, Year, Yesterday,
};

use crate::null_propagate;
use crate::registry::EvalRegistry;
use crate::reverse_eval::domain::Constraint;
use crate::value::Value;
use crate::value::{TimeZone, TimestampValue};

/// Register all datetime function eval implementations.
pub fn register(registry: &mut EvalRegistry) {
    // now() - current timestamp
    registry.register_eval::<Now>(|_params| Ok(TimestampValue::utc(chrono::Utc::now()).into()));

    // today() - start of current day
    registry.register_eval::<Today>(|_params| {
        let now = chrono::Utc::now();
        let date = now.date_naive();
        let midnight = date
            .and_hms_opt(0, 0, 0)
            .ok_or_else(|| anyhow::anyhow!("failed to create midnight timestamp"))?;
        Ok(TimestampValue::utc(midnight.and_utc()).into())
    });

    // yesterday() - start of previous day
    registry.register_eval::<Yesterday>(|_params| {
        let now = chrono::Utc::now();
        let yesterday_date = now.date_naive() - chrono::Duration::days(1);
        let midnight = yesterday_date
            .and_hms_opt(0, 0, 0)
            .ok_or_else(|| anyhow::anyhow!("failed to create midnight timestamp"))?;
        Ok(TimestampValue::utc(midnight.and_utc()).into())
    });

    // tomorrow() - start of next day
    registry.register_eval::<Tomorrow>(|_params| {
        let now = chrono::Utc::now();
        let tomorrow_date = now.date_naive() + chrono::Duration::days(1);
        let midnight = tomorrow_date
            .and_hms_opt(0, 0, 0)
            .ok_or_else(|| anyhow::anyhow!("failed to create midnight timestamp"))?;
        Ok(TimestampValue::utc(midnight.and_utc()).into())
    });

    // ts(string) - parse timestamp from string
    registry.register_eval::<Ts>(|mut params| {
        let s = null_propagate!(params.take()?).require_string()?;

        // First try to parse with timezone information (preserving the offset)
        if let Ok(dt_with_tz) = chrono::DateTime::parse_from_rfc3339(&s) {
            let instant = dt_with_tz.with_timezone(&chrono::Utc);
            let timezone = TimeZone::FixedOffset(*dt_with_tz.offset());
            return Ok(TimestampValue::new(instant, timezone).into());
        }

        // Try other timezone-aware formats (preserving the offset)
        let tz_formats = &["%Y-%m-%d %H:%M:%S%z", "%Y-%m-%dT%H:%M:%S%z"];

        for fmt in tz_formats {
            if let Ok(dt) = chrono::DateTime::parse_from_str(&s, fmt) {
                let instant = dt.with_timezone(&chrono::Utc);
                let timezone = TimeZone::FixedOffset(*dt.offset());
                return Ok(TimestampValue::new(instant, timezone).into());
            }
        }

        // Try formats without explicit timezone (assume UTC)
        let naive_formats = &[
            "%Y-%m-%d %H:%M:%S",
            "%Y-%m-%dT%H:%M:%S",
            "%Y-%m-%d %H:%M:%S%.f",
            "%Y-%m-%dT%H:%M:%S%.f",
            "%Y-%m-%d",
            "%Y/%m/%d %H:%M:%S",
            "%Y/%m/%d",
        ];

        for fmt in naive_formats {
            if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(&s, fmt) {
                return Ok(TimestampValue::utc(naive.and_utc()).into());
            }
        }

        // Also try parsing just a date
        if let Ok(date) = chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d") {
            let naive = date
                .and_hms_opt(0, 0, 0)
                .ok_or_else(|| anyhow::anyhow!("failed to create midnight timestamp"))?;
            return Ok(TimestampValue::utc(naive.and_utc()).into());
        }
        if let Ok(date) = chrono::NaiveDate::parse_from_str(&s, "%Y/%m/%d") {
            let naive = date
                .and_hms_opt(0, 0, 0)
                .ok_or_else(|| anyhow::anyhow!("failed to create midnight timestamp"))?;
            return Ok(TimestampValue::utc(naive.and_utc()).into());
        }

        Err(anyhow::anyhow!(
            "Failed to parse timestamp from string: {}",
            s
        ))
    });

    // year(timestamp) - extract year
    registry.register_eval::<Year>(|mut params| {
        let timestamp = null_propagate!(params.take()?);
        let ts = timestamp.try_timestamp()?;
        Ok(Value::Int(ts.year()? as i64))
    });

    // month(timestamp) - extract month
    registry.register_eval::<Month>(|mut params| {
        let ts = null_propagate!(params.take()?).require_timestamp()?;
        Ok(Value::Int(ts.month()? as i64))
    });

    // day(timestamp) - extract day
    registry.register_eval::<Day>(|mut params| {
        let ts = null_propagate!(params.take()?).require_timestamp()?;
        Ok(Value::Int(ts.day()? as i64))
    });

    // day_of_week(timestamp) - extract day of week (1=Sunday, 7=Saturday)
    registry.register_eval::<DayOfWeek>(|mut params| {
        let ts = null_propagate!(params.take()?).require_timestamp()?;
        // Convert Chrono's Weekday to SQL standard (1=Sunday, 7=Saturday)
        let weekday = match ts.weekday()? {
            chrono::Weekday::Sun => 1,
            chrono::Weekday::Mon => 2,
            chrono::Weekday::Tue => 3,
            chrono::Weekday::Wed => 4,
            chrono::Weekday::Thu => 5,
            chrono::Weekday::Fri => 6,
            chrono::Weekday::Sat => 7,
        };
        Ok(Value::Int(weekday))
    });

    // hour(timestamp) - extract hour
    registry.register_eval::<Hour>(|mut params| {
        let ts = null_propagate!(params.take()?).require_timestamp()?;
        Ok(Value::Int(ts.hour()? as i64))
    });

    // minute(timestamp) - extract minute
    registry.register_eval::<Minute>(|mut params| {
        let ts = null_propagate!(params.take()?).require_timestamp()?;
        Ok(Value::Int(ts.minute()? as i64))
    });

    // second(timestamp) - extract second
    registry.register_eval::<Second>(|mut params| {
        let ts = null_propagate!(params.take()?).require_timestamp()?;
        Ok(Value::Int(ts.second()? as i64))
    });

    // at_timezone(timestamp, timezone) - convert to specified timezone
    registry.register_eval::<AtTimezone>(|mut params| {
        let ts = null_propagate!(params.take()?).require_timestamp()?;
        let tz_str = null_propagate!(params.take()?).require_string()?;

        // Parse timezone string to Tz
        let tz: chrono_tz::Tz = tz_str
            .parse()
            .map_err(|_| anyhow::anyhow!("Invalid timezone name: {}", tz_str))?;

        // Create new TimestampValue with same instant, new timezone
        Ok(ts.with_timezone(TimeZone::Named(tz)).into())
    });

    // at_timezone reverse eval
    registry.register_reverse::<AtTimezone>(|output_constraint, mut params| {
        // at_timezone(ts, tz) = constraint
        // If we know the target timezone, we can reverse it
        // The key insight: the instant is preserved, only display changes
        // So if constraint is a specific timestamp, we just need to check
        // that the instant matches (timezone is irrelevant for filtering)
        let tz_str = match params.take()? {
            Some(Value::String(s)) => s,
            Some(Value::Null) => return Ok(Constraint::Universal),
            None => return Ok(Constraint::Universal), // variable timezone, can't constrain
            _ => return Ok(Constraint::Universal),
        };

        // If the second parameter is known, we can try to reverse
        // but for simplicity, return the constraint as-is for the timestamp input
        // The instant must match, so the constraint on input equals the output constraint
        let tz: chrono_tz::Tz = match tz_str.parse() {
            Ok(tz) => tz,
            Err(_) => return Ok(Constraint::Empty), // invalid timezone
        };

        // For at_timezone, the input constraint is the same as output,
        // just with potentially different timezone display
        output_constraint.map(|val| match val {
            Value::Timestamp(ts) => Ok(ts.clone().with_timezone(TimeZone::Named(tz)).into()),
            _ => Err(anyhow::anyhow!(
                "Expected timestamp value for at_timezone reverse"
            )),
        })
    });

    // to_millis(interval) - convert interval to milliseconds
    registry.register_eval::<ToMillis>(|mut params| {
        let interval_value = null_propagate!(params.take()?).require_interval()?;
        let millis = interval_value.num_milliseconds();
        Ok(Value::Int(millis))
    });

    // from_unixtime_seconds(seconds) - create timestamp from unix seconds
    registry.register_eval::<FromUnixtimeSeconds>(|mut params| {
        let secs = null_propagate!(params.take()?).require_int()?;
        let dt = chrono::DateTime::from_timestamp(secs, 0)
            .ok_or_else(|| anyhow::anyhow!("Invalid timestamp seconds: {}", secs))?;
        Ok(TimestampValue::utc(dt).into())
    });

    // from_unixtime_seconds reverse eval
    registry.register_reverse::<FromUnixtimeSeconds>(|output_constraint, _params| {
        // from_unixtime_seconds(epoch_col) = timestamp_constraint
        // => epoch_col = timestamp_to_seconds(timestamp_constraint)
        output_constraint.map(|timestamp_val| match timestamp_val {
            Value::Timestamp(ts) => Ok(Value::Int(ts.timestamp())),
            _ => Err(anyhow::anyhow!(
                "Expected timestamp value for reverse evaluation of from_unixtime_seconds"
            )),
        })
    });

    // from_unixtime_millis(millis) - create timestamp from unix milliseconds
    registry.register_eval::<FromUnixtimeMillis>(|mut params| {
        let ms = null_propagate!(params.take()?).require_int()?;
        let dt = chrono::DateTime::from_timestamp_millis(ms)
            .ok_or_else(|| anyhow::anyhow!("Invalid timestamp milliseconds: {}", ms))?;
        Ok(TimestampValue::utc(dt).into())
    });

    // from_unixtime_millis reverse eval
    registry.register_reverse::<FromUnixtimeMillis>(|output_constraint, _params| {
        // from_unixtime_millis(epoch_col) = timestamp_constraint
        // => epoch_col = timestamp_to_millis(timestamp_constraint)
        output_constraint.map(|timestamp_val| match timestamp_val {
            Value::Timestamp(ts) => Ok(Value::Int(ts.timestamp_millis())),
            _ => Err(anyhow::anyhow!(
                "Expected timestamp value for reverse evaluation of from_unixtime_millis"
            )),
        })
    });

    // from_unixtime_micros(micros) - create timestamp from unix microseconds
    registry.register_eval::<FromUnixtimeMicros>(|mut params| {
        let us = null_propagate!(params.take()?).require_int()?;
        let dt = chrono::DateTime::from_timestamp_micros(us)
            .ok_or_else(|| anyhow::anyhow!("Invalid timestamp microseconds: {}", us))?;
        Ok(TimestampValue::utc(dt).into())
    });

    // from_unixtime_micros reverse eval
    registry.register_reverse::<FromUnixtimeMicros>(|output_constraint, _params| {
        // from_unixtime_micros(epoch_col) = timestamp_constraint
        // => epoch_col = timestamp_to_micros(timestamp_constraint)
        output_constraint.map(|timestamp_val| match timestamp_val {
            Value::Timestamp(ts) => Ok(Value::Int(ts.timestamp_micros())),
            _ => Err(anyhow::anyhow!(
                "Expected timestamp value for reverse evaluation of from_unixtime_micros"
            )),
        })
    });

    // from_unixtime_nanos(nanos) - create timestamp from unix nanoseconds
    registry.register_eval::<FromUnixtimeNanos>(|mut params| {
        let ns = null_propagate!(params.take()?).require_int()?;
        let dt = chrono::DateTime::from_timestamp_nanos(ns);
        Ok(TimestampValue::utc(dt).into())
    });

    // from_unixtime_nanos reverse eval
    registry.register_reverse::<FromUnixtimeNanos>(|output_constraint, _params| {
        // from_unixtime_nanos(epoch_col) = timestamp_constraint
        // => epoch_col = timestamp_to_nanos(timestamp_constraint)
        output_constraint.map(|timestamp_val| match timestamp_val {
            Value::Timestamp(ts) => Ok(Value::Int(ts.timestamp_nanos_opt().unwrap_or(0))),
            _ => Err(anyhow::anyhow!(
                "Expected timestamp value for reverse evaluation of from_unixtime_nanos"
            )),
        })
    });

    // to_unixtime(timestamp) - convert timestamp to unix seconds as double
    registry.register_eval::<ToUnixtime>(|mut params| {
        let ts = null_propagate!(params.take()?).require_timestamp()?;
        let secs = ts.timestamp() as f64 + (ts.timestamp_subsec_micros() as f64 / 1_000_000.0);
        Ok(Value::Double(secs))
    });

    // to_unixtime reverse eval
    registry.register_reverse::<ToUnixtime>(|output_constraint, _params| {
        // to_unixtime(timestamp_col) = double_constraint
        // => timestamp_col = from_unixtime_seconds(double_constraint)
        output_constraint.map(|double_val| match double_val {
            Value::Double(secs) => {
                // Use from_timestamp_opt with microsecond precision to handle f64 accurately
                let micros = (*secs * 1_000_000.0).round() as i64;
                let ts = chrono::DateTime::from_timestamp_micros(micros)
                    .ok_or_else(|| anyhow::anyhow!("Invalid unix timestamp: {}", secs))?;
                Ok(TimestampValue::utc(ts).into())
            }
            Value::Int(secs) => {
                let ts = chrono::DateTime::from_timestamp(*secs, 0)
                    .ok_or_else(|| anyhow::anyhow!("Invalid unix timestamp: {}", secs))?;
                Ok(TimestampValue::utc(ts).into())
            }
            _ => Err(anyhow::anyhow!(
                "Expected numeric value for reverse evaluation of to_unixtime"
            )),
        })
    });
}