use crate::eval::error::EvalResult;
use crate::eval::evaluator::eval;
use crate::eval::tests::test_helpers::*;
use crate::value::{TimeZone, TimestampValue, Value};
use chrono::{DateTime, Duration, FixedOffset, TimeZone as _, Timelike, Utc};
use hamelin_lib::tree::builder::{
add, at_day, at_hour, at_minute, at_month, at_second, at_week, at_year, call, days, divide, eq,
field_ref, hours, int, lt, minutes, months, multiply, null, string, subtract, years,
ExpressionBuilder,
};
use hamelin_lib::tree::options::ExpressionTypeCheckOptions;
use hamelin_lib::type_check_expression;
use hamelin_lib::types::{INT, TIMESTAMP};
use rstest::rstest;
use std::sync::Arc;
#[test]
fn test_eval_now_function() {
let ctx = TestContext::default();
let expr =
type_check_expression(call("now").build(), ExpressionTypeCheckOptions::default()).output;
let result = eval(&expr, &ctx.env).unwrap();
match result {
Value::Timestamp(ts) => {
assert!(ts.timestamp() > 0);
}
_ => panic!("Expected Timestamp value"),
}
}
#[test]
fn test_eval_ts_function() {
let ctx = TestContext::default();
let expr = type_check_expression(
call("ts").arg(string("2023-12-25T15:30:45Z")).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
match result {
Value::Timestamp(ts) => {
assert!(ts.timestamp_micros() > 1_703_000_000_000_000); }
_ => panic!("Expected Timestamp value"),
}
}
#[test]
fn test_eval_ts_function_with_timezone() {
let ctx = TestContext::default();
let expr = type_check_expression(
call("ts").arg(string("2023-12-25T15:30:45+05:00")).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let ts = result.require_timestamp().unwrap();
assert!(ts.timestamp() > 0);
assert_eq!(ts.hour().unwrap(), 15);
let expected_utc_instant = Utc.with_ymd_and_hms(2023, 12, 25, 10, 30, 45).unwrap();
assert_eq!(*ts.instant(), expected_utc_instant);
}
#[test]
fn test_eval_year_function() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2023, 12, 25, 0, 0, 0).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
call("year").arg(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
assert_eq!(result, Value::Int(2023));
}
#[test]
fn test_eval_timestamp_comparison() {
let mut ctx = TestContext::default();
let ts1 = TimestampValue::utc(DateTime::from_timestamp(1_703_520_000, 0).unwrap()).into();
let ts2 = TimestampValue::utc(DateTime::from_timestamp(1_703_530_000, 0).unwrap()).into();
ctx.set("ts1", ts1, TIMESTAMP);
ctx.set("ts2", ts2, TIMESTAMP);
let expr = type_check_expression(
lt(field_ref("ts1"), field_ref("ts2")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
assert_eq!(result, Value::Boolean(true));
let expr = type_check_expression(
eq(field_ref("ts1"), field_ref("ts1")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
assert_eq!(result, Value::Boolean(true));
}
#[test]
fn test_eval_from_unixtime_seconds() {
let ctx = TestContext::default();
let expr = type_check_expression(
call("from_unixtime_seconds").arg(int(1703520000)).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
match result {
Value::Timestamp(ts) => {
assert_eq!(ts.timestamp_micros(), 1_703_520_000_000_000);
}
_ => panic!("Expected Timestamp value"),
}
}
#[test]
fn test_eval_to_unixtime() {
let mut ctx = TestContext::default();
let ts = DateTime::from_timestamp(1_703_520_000, 0)
.unwrap()
.with_timezone(&Utc);
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
call("to_unixtime").arg(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
assert_eq!(result, Value::Double(1703520000.0));
}
#[test]
fn test_eval_ts_function_various_formats() {
let ctx = TestContext::default();
let test_cases = vec![
"2023-01-01T00:00:00Z",
"2023-06-15T12:30:45.123Z",
"2023-12-31T23:59:59-08:00",
"2023-07-04T16:20:30+02:00",
];
for timestamp_str in test_cases {
let expr = type_check_expression(
call("ts").arg(string(timestamp_str)).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env);
assert!(
result.is_ok(),
"Failed to parse timestamp: {}",
timestamp_str
);
match result.unwrap() {
Value::Timestamp(ts) => {
assert!(
ts.timestamp() > 0,
"Invalid timestamp for: {}",
timestamp_str
);
}
_ => panic!("Expected Timestamp value for: {}", timestamp_str),
}
}
}
#[test]
fn test_eval_ts_function_invalid_format() {
let ctx = TestContext::default();
let expr = type_check_expression(
call("ts").arg(string("not-a-timestamp")).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env);
assert!(result.is_err());
}
#[test]
fn test_eval_year_function_different_years() {
let test_years = vec![2020, 2021, 2022, 2023, 2024];
for year in test_years {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(year, 6, 15, 12, 0, 0).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
call("year").arg(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
assert_eq!(
result,
Value::Int(year as i64),
"Year extraction failed for {}",
year
);
}
}
#[test]
fn test_eval_from_unixtime_seconds_edge_cases() {
let ctx = TestContext::default();
let expr_zero = type_check_expression(
call("from_unixtime_seconds").arg(int(0)).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr_zero, &ctx.env).unwrap();
match result {
Value::Timestamp(ts) => {
assert_eq!(ts.timestamp(), 0);
}
_ => panic!("Expected Timestamp value"),
}
let expr_large = type_check_expression(
call("from_unixtime_seconds").arg(int(2147483647)).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr_large, &ctx.env).unwrap();
match result {
Value::Timestamp(ts) => {
assert_eq!(ts.timestamp(), 2147483647);
}
_ => panic!("Expected Timestamp value"),
}
}
#[test]
fn test_eval_timestamp_ordering() {
let mut ctx = TestContext::default();
let early = TimestampValue::utc(DateTime::from_timestamp(1_000_000_000, 0).unwrap());
let middle = TimestampValue::utc(DateTime::from_timestamp(1_500_000_000, 0).unwrap());
let late = TimestampValue::utc(DateTime::from_timestamp(2_000_000_000, 0).unwrap());
ctx.set("early", early.into(), TIMESTAMP);
ctx.set("middle", middle.into(), TIMESTAMP);
ctx.set("late", late.into(), TIMESTAMP);
let expr1 = type_check_expression(
lt(field_ref("early"), field_ref("middle")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
assert_eq!(eval(&expr1, &ctx.env).unwrap(), Value::Boolean(true));
let expr2 = type_check_expression(
lt(field_ref("middle"), field_ref("late")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
assert_eq!(eval(&expr2, &ctx.env).unwrap(), Value::Boolean(true));
let expr3 = type_check_expression(
lt(field_ref("early"), field_ref("late")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
assert_eq!(eval(&expr3, &ctx.env).unwrap(), Value::Boolean(true));
let expr4 = type_check_expression(
lt(field_ref("late"), field_ref("early")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
assert_eq!(eval(&expr4, &ctx.env).unwrap(), Value::Boolean(false));
}
#[test]
fn test_eval_timestamp_precision() {
let ctx = TestContext::default();
let expr = type_check_expression(
call("ts")
.arg(string("2023-12-25T15:30:45.123456Z"))
.build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env);
assert!(result.is_ok());
match result.unwrap() {
Value::Timestamp(ts) => {
let micros = ts.timestamp_micros();
assert_eq!(micros % 1_000_000, 123456); }
_ => panic!("Expected Timestamp value"),
}
}
#[test]
fn test_eval_ts_trunc_to_second() {
let mut ctx = TestContext::default();
let ts = Utc
.with_ymd_and_hms(2024, 3, 15, 14, 30, 45)
.unwrap()
.with_nanosecond(123456000)
.unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
at_second(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
assert_eq!(result, TimestampValue::utc(expected).into());
}
#[test]
fn test_eval_ts_trunc_to_minute() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
at_minute(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 0).unwrap();
assert_eq!(result, TimestampValue::utc(expected).into());
}
#[test]
fn test_eval_ts_trunc_to_hour() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
at_hour(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 15, 14, 0, 0).unwrap();
assert_eq!(result, TimestampValue::utc(expected).into());
}
#[test]
fn test_eval_ts_trunc_to_day() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
at_day(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 15, 0, 0, 0).unwrap();
assert_eq!(result, TimestampValue::utc(expected).into());
}
#[test]
fn test_eval_ts_trunc_to_week() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
at_week(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 11, 0, 0, 0).unwrap();
assert_eq!(result, TimestampValue::utc(expected).into());
}
#[test]
fn test_eval_ts_trunc_to_month() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
at_month(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 1, 0, 0, 0).unwrap();
assert_eq!(result, TimestampValue::utc(expected).into());
}
#[test]
fn test_eval_ts_trunc_to_year() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
at_year(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
assert_eq!(result, TimestampValue::utc(expected).into());
}
#[test]
fn test_eval_ts_trunc_different_months() {
let test_cases = vec![
(
Utc.with_ymd_and_hms(2024, 2, 15, 14, 30, 45).unwrap(),
Utc.with_ymd_and_hms(2024, 2, 1, 0, 0, 0).unwrap(),
),
(
Utc.with_ymd_and_hms(2024, 5, 20, 9, 15, 30).unwrap(),
Utc.with_ymd_and_hms(2024, 5, 1, 0, 0, 0).unwrap(),
),
(
Utc.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap(),
Utc.with_ymd_and_hms(2024, 12, 1, 0, 0, 0).unwrap(),
),
];
for (input_ts, expected_ts) in test_cases {
let mut ctx = TestContext::default();
ctx.set("ts", TimestampValue::utc(input_ts).into(), TIMESTAMP);
let expr = type_check_expression(
at_month(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
assert_eq!(result, TimestampValue::utc(expected_ts).into());
}
}
#[test]
fn test_eval_ts_trunc_edge_cases() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
at_year(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
assert_eq!(result, TimestampValue::utc(ts).into());
let mut ctx2 = TestContext::default();
let ts_end = Utc.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap();
ctx2.set("ts", TimestampValue::utc(ts_end).into(), TIMESTAMP);
let expr = type_check_expression(
at_year(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx2.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx2.env).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
assert_eq!(result, TimestampValue::utc(expected).into());
}
#[test]
fn test_eval_ts_trunc_with_constants() {
let ctx = TestContext::default();
let ts_expr = call("ts").arg(string("2024-03-15T14:30:45.123Z"));
let expr = type_check_expression(
at_day(ts_expr).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 15, 0, 0, 0).unwrap();
assert_eq!(result, TimestampValue::utc(expected).into());
}
#[test]
fn test_eval_ts_trunc_error_handling() {
let mut ctx = TestContext::default();
ctx.set("not_ts", Value::Int(42), INT);
let expr = type_check_expression(
at_day(field_ref("not_ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env);
assert!(result.is_err());
}
#[test]
fn test_eval_timestamp_plus_calendar_interval_months() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 1, 15, 14, 30, 0).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
add(field_ref("ts"), months(2)).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 0).unwrap();
assert_eq!(result, TimestampValue::utc(expected).into());
}
#[test]
fn test_eval_calendar_interval_plus_timestamp() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 2, 20, 10, 0, 0).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
add(years(1), field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let expected = Utc.with_ymd_and_hms(2025, 2, 20, 10, 0, 0).unwrap();
assert_eq!(result, TimestampValue::utc(expected).into());
}
#[test]
fn test_eval_timestamp_minus_calendar_interval() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 5, 31, 12, 0, 0).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
subtract(field_ref("ts"), months(3)).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let expected = Utc.with_ymd_and_hms(2024, 2, 29, 12, 0, 0).unwrap();
assert_eq!(result, TimestampValue::utc(expected).into());
}
#[test]
fn test_eval_calendar_interval_edge_case_month_end() {
let ctx = TestContext::default();
let expr = type_check_expression(
add(call("ts").arg(string("2024-01-31T00:00:00Z")), months(1)).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let result_ts = result.require_timestamp().unwrap();
assert_eq!(result_ts.month().unwrap(), 2, "Expected February");
assert_eq!(
result_ts.day().unwrap(),
29,
"Expected February 29 (clamped from 31)"
);
}
#[test]
fn test_eval_interval_plus_interval() {
let ctx = TestContext::default();
let expr = type_check_expression(
add(hours(2), minutes(30)).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
assert_eq!(result, Value::Interval(Duration::minutes(150)));
}
#[test]
fn test_eval_interval_minus_interval() {
let ctx = TestContext::default();
let expr = type_check_expression(
subtract(days(5), days(2)).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
assert_eq!(result, Value::Interval(Duration::days(3)));
}
#[test]
fn test_eval_interval_multiply_numeric() {
let ctx = TestContext::default();
let expr = type_check_expression(
multiply(hours(3), int(2)).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
assert_eq!(result, Value::Interval(Duration::hours(6)));
}
#[test]
fn test_eval_numeric_multiply_interval() {
let ctx = TestContext::default();
let expr = type_check_expression(
multiply(int(4), minutes(15)).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
assert_eq!(result, Value::Interval(Duration::minutes(60)));
}
#[test]
fn test_eval_interval_divide_numeric() {
let ctx = TestContext::default();
let expr = type_check_expression(
divide(days(10), int(2)).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
assert_eq!(result, Value::Interval(Duration::days(5)));
}
#[test]
fn test_eval_calendar_interval_plus_calendar_interval() {
let ctx = TestContext::default();
let expr = type_check_expression(
add(years(1), months(6)).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
match result {
Value::CalendarInterval(months) => {
assert_eq!(months, 18, "Expected 18 months");
}
_ => panic!("Expected CalendarInterval value, got {:?}", result),
}
}
#[test]
fn test_eval_calendar_interval_minus_calendar_interval() {
let ctx = TestContext::default();
let expr = type_check_expression(
subtract(months(24), years(1)).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
match result {
Value::CalendarInterval(months) => {
assert_eq!(months, 12, "Expected 12 months");
}
_ => panic!("Expected CalendarInterval value, got {:?}", result),
}
}
#[test]
fn test_eval_calendar_interval_multiply_numeric() {
let ctx = TestContext::default();
let expr = type_check_expression(
multiply(months(6), 2).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
match result {
Value::CalendarInterval(months) => {
assert_eq!(months, 12, "Expected 12 months");
}
_ => panic!("Expected CalendarInterval value, got {:?}", result),
}
}
#[test]
fn test_eval_numeric_multiply_calendar_interval() {
let ctx = TestContext::default();
let expr = type_check_expression(
multiply(3, months(4)).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
match result {
Value::CalendarInterval(months) => {
assert_eq!(months, 12, "Expected 12 months");
}
_ => panic!("Expected CalendarInterval value, got {:?}", result),
}
}
#[test]
fn test_eval_calendar_interval_divide_numeric() {
let ctx = TestContext::default();
let expr = type_check_expression(
divide(months(12), 3).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
match result {
Value::CalendarInterval(months) => {
assert_eq!(months, 4, "Expected 4 months");
}
_ => panic!("Expected CalendarInterval value, got {:?}", result),
}
}
#[test]
fn test_eval_at_timezone_function_utc_to_america_new_york() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 1, 15, 20, 0, 0).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
call("at_timezone")
.arg(field_ref("ts"))
.arg(string("America/New_York"))
.build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
match result {
Value::Timestamp(result_ts) => {
assert_eq!(*result_ts.instant(), ts);
assert_eq!(result_ts.hour().unwrap(), 15);
assert_eq!(result_ts.year().unwrap(), 2024);
assert_eq!(result_ts.month().unwrap(), 1);
assert_eq!(result_ts.day().unwrap(), 15);
}
_ => panic!("Expected Timestamp value, got {:?}", result),
}
}
#[test]
fn test_eval_at_timezone_function_utc_to_asia_tokyo() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 1, 15, 15, 0, 0).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
call("at_timezone")
.arg(field_ref("ts"))
.arg(string("Asia/Tokyo"))
.build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
match result {
Value::Timestamp(result_ts) => {
assert_eq!(*result_ts.instant(), ts);
assert_eq!(result_ts.hour().unwrap(), 0);
assert_eq!(result_ts.year().unwrap(), 2024);
assert_eq!(result_ts.month().unwrap(), 1);
assert_eq!(result_ts.day().unwrap(), 16); }
_ => panic!("Expected Timestamp value, got {:?}", result),
}
}
#[test]
fn test_eval_at_timezone_function_preserves_instant() {
let mut ctx = TestContext::default();
let ts1 = Utc.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
let ts2 = Utc.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
ctx.set("ts1", TimestampValue::utc(ts1).into(), TIMESTAMP);
ctx.set("ts2", TimestampValue::utc(ts2).into(), TIMESTAMP);
let expr1 = type_check_expression(
call("at_timezone")
.arg(field_ref("ts1"))
.arg(string("Europe/London"))
.build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let expr2 = type_check_expression(
call("at_timezone")
.arg(field_ref("ts2"))
.arg(string("Pacific/Auckland"))
.build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result1 = eval(&expr1, &ctx.env).unwrap();
let result2 = eval(&expr2, &ctx.env).unwrap();
match (result1, result2) {
(Value::Timestamp(ts1), Value::Timestamp(ts2)) => {
assert_eq!(*ts1.instant(), *ts2.instant());
}
_ => panic!("Expected Timestamp values"),
}
}
#[test]
fn test_eval_at_timezone_function_invalid_timezone() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
call("at_timezone")
.arg(field_ref("ts"))
.arg(string("Invalid/Timezone"))
.build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env);
assert!(result.is_err(), "Should error on invalid timezone");
}
#[test]
fn test_eval_at_timezone_function_null_handling() {
let ctx = TestContext::default();
let expr = type_check_expression(
call("at_timezone")
.arg(null().build())
.arg(string("America/New_York"))
.build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
assert_eq!(result, Value::Null);
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
call("at_timezone")
.arg(field_ref("ts"))
.arg(null().build())
.build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
assert_eq!(result, Value::Null);
}
#[test]
fn test_eval_ts_function_preserves_timezone_for_component_extraction() {
let ctx = TestContext::default();
let expr_pst = type_check_expression(
call("ts").arg(string("2024-01-01T02:00:00-08:00")).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result_pst = eval(&expr_pst, &ctx.env).unwrap();
let ts_pst = result_pst.require_timestamp().unwrap();
assert_eq!(ts_pst.hour().unwrap(), 2, "Hour should be 2 in PST");
assert_eq!(ts_pst.day().unwrap(), 1, "Day should be 1 in PST");
assert_eq!(ts_pst.month().unwrap(), 1, "Month should be January in PST");
let expected_instant = Utc.with_ymd_and_hms(2024, 1, 1, 10, 0, 0).unwrap();
assert_eq!(*ts_pst.instant(), expected_instant);
let expr_ist = type_check_expression(
call("ts").arg(string("2024-01-01T15:30:00+05:30")).build(),
ExpressionTypeCheckOptions::default(),
)
.output;
let result_ist = eval(&expr_ist, &ctx.env).unwrap();
let ts_ist = result_ist.require_timestamp().unwrap();
assert_eq!(ts_ist.hour().unwrap(), 15, "Hour should be 15 in IST");
assert_eq!(ts_ist.minute().unwrap(), 30, "Minute should be 30 in IST");
assert_eq!(ts_pst, ts_ist, "Same instant should be equal");
assert_ne!(
ts_pst.hour().unwrap(),
ts_ist.hour().unwrap(),
"Hours should differ due to timezone"
);
}
#[test]
fn test_eval_ts_trunc_respects_timezone_day_boundary() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 1, 15, 2, 30, 0).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
at_day(
call("at_timezone")
.arg(field_ref("ts"))
.arg(string("America/New_York"))
.build(),
)
.build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let result_ts = result.require_timestamp().unwrap();
let expected = Utc.with_ymd_and_hms(2024, 1, 14, 5, 0, 0).unwrap();
assert_eq!(
*result_ts.instant(),
expected,
"Should truncate to midnight in NYC timezone (2024-01-14 00:00:00 EST = 2024-01-14 05:00:00 UTC)"
);
assert_eq!(result_ts.year().unwrap(), 2024);
assert_eq!(result_ts.month().unwrap(), 1);
assert_eq!(result_ts.day().unwrap(), 14);
assert_eq!(result_ts.hour().unwrap(), 0);
let expr_utc = type_check_expression(
at_day(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result_utc = eval(&expr_utc, &ctx.env).unwrap();
let result_ts_utc = result_utc.require_timestamp().unwrap();
let expected_utc = Utc.with_ymd_and_hms(2024, 1, 15, 0, 0, 0).unwrap();
assert_eq!(
*result_ts_utc.instant(),
expected_utc,
"UTC truncation should give 2024-01-15 00:00:00 UTC"
);
}
#[test]
fn test_eval_ts_trunc_respects_timezone_week_boundary() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 1, 15, 2, 0, 0).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
at_week(
call("at_timezone")
.arg(field_ref("ts"))
.arg(string("America/Los_Angeles"))
.build(),
)
.build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let result_ts = result.require_timestamp().unwrap();
let expected = Utc.with_ymd_and_hms(2024, 1, 8, 8, 0, 0).unwrap();
assert_eq!(
*result_ts.instant(),
expected,
"Should truncate to Monday in LA timezone (2024-01-08 00:00:00 PST = 2024-01-08 08:00:00 UTC)"
);
assert_eq!(result_ts.year().unwrap(), 2024);
assert_eq!(result_ts.month().unwrap(), 1);
assert_eq!(result_ts.day().unwrap(), 8);
}
#[test]
fn test_eval_ts_trunc_respects_timezone_month_boundary() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 2, 1, 2, 0, 0).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
at_month(
call("at_timezone")
.arg(field_ref("ts"))
.arg(string("America/New_York"))
.build(),
)
.build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let result_ts = result.require_timestamp().unwrap();
let expected = Utc.with_ymd_and_hms(2024, 1, 1, 5, 0, 0).unwrap();
assert_eq!(
*result_ts.instant(),
expected,
"Should truncate to January 1st in NYC timezone (2024-01-01 00:00:00 EST = 2024-01-01 05:00:00 UTC)"
);
assert_eq!(result_ts.year().unwrap(), 2024);
assert_eq!(result_ts.month().unwrap(), 1);
assert_eq!(result_ts.day().unwrap(), 1);
let expr_utc = type_check_expression(
at_month(field_ref("ts")).build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result_utc = eval(&expr_utc, &ctx.env).unwrap();
let result_ts_utc = result_utc.require_timestamp().unwrap();
let expected_utc = Utc.with_ymd_and_hms(2024, 2, 1, 0, 0, 0).unwrap();
assert_eq!(
*result_ts_utc.instant(),
expected_utc,
"UTC truncation should give February 1st"
);
}
#[test]
fn test_eval_ts_trunc_hour_preserves_timezone() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 6, 15, 14, 45, 30).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
at_hour(
call("at_timezone")
.arg(field_ref("ts"))
.arg(string("Asia/Tokyo"))
.build(),
)
.build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let result_ts = result.require_timestamp().unwrap();
let expected = Utc.with_ymd_and_hms(2024, 6, 15, 14, 0, 0).unwrap();
assert_eq!(
*result_ts.instant(),
expected,
"Should truncate to hour in Tokyo timezone"
);
assert_eq!(result_ts.hour().unwrap(), 23);
assert_eq!(result_ts.minute().unwrap(), 0);
}
#[test]
fn test_eval_ts_trunc_across_dst_transition() {
let mut ctx = TestContext::default();
let ts = Utc.with_ymd_and_hms(2024, 3, 10, 8, 0, 0).unwrap();
ctx.set("ts", TimestampValue::utc(ts).into(), TIMESTAMP);
let expr = type_check_expression(
at_day(
call("at_timezone")
.arg(field_ref("ts"))
.arg(string("America/New_York"))
.build(),
)
.build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx.translation_env.clone()))
.build(),
)
.output;
let result = eval(&expr, &ctx.env).unwrap();
let result_ts = result.require_timestamp().unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 10, 5, 0, 0).unwrap();
assert_eq!(
*result_ts.instant(),
expected,
"Should truncate to midnight on DST transition day (2024-03-10 00:00:00 EST = 2024-03-10 05:00:00 UTC)"
);
assert_eq!(result_ts.year().unwrap(), 2024);
assert_eq!(result_ts.month().unwrap(), 3);
assert_eq!(result_ts.day().unwrap(), 10);
assert_eq!(result_ts.hour().unwrap(), 0);
let mut ctx2 = TestContext::default();
let ts_before = Utc.with_ymd_and_hms(2024, 3, 10, 6, 0, 0).unwrap();
ctx2.set("ts", TimestampValue::utc(ts_before).into(), TIMESTAMP);
let expr2 = type_check_expression(
at_day(
call("at_timezone")
.arg(field_ref("ts"))
.arg(string("America/New_York"))
.build(),
)
.build(),
ExpressionTypeCheckOptions::builder()
.bindings(Arc::new(ctx2.translation_env.clone()))
.build(),
)
.output;
let result_before = eval(&expr2, &ctx2.env).unwrap();
let result_ts_before = result_before.require_timestamp().unwrap();
let expected = Utc.with_ymd_and_hms(2024, 3, 10, 5, 0, 0).unwrap();
assert_eq!(
*result_ts_before.instant(),
expected,
"Before DST transition should also truncate to midnight"
);
}
#[rstest]
#[case::year(|ts: &TimestampValue| ts.year().map(|_| ()))]
#[case::month(|ts: &TimestampValue| ts.month().map(|_| ()))]
#[case::day(|ts: &TimestampValue| ts.day().map(|_| ()))]
#[case::hour(|ts: &TimestampValue| ts.hour().map(|_| ()))]
#[case::minute(|ts: &TimestampValue| ts.minute().map(|_| ()))]
#[case::second(|ts: &TimestampValue| ts.second().map(|_| ()))]
#[case::weekday(|ts: &TimestampValue| ts.weekday().map(|_| ()))]
#[case::to_datetime_tz(|ts: &TimestampValue| ts.to_datetime_tz().map(|_| ()))]
#[case::to_datetime_fixed(|ts: &TimestampValue| ts.to_datetime_fixed().map(|_| ()))]
fn test_timezone_any_operations_error<F>(#[case] operation: F)
where
F: Fn(&TimestampValue) -> EvalResult<()>,
{
let ts = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 0).unwrap();
let ts_any = TimestampValue::new(ts, TimeZone::Any);
let result = operation(&ts_any);
assert!(result.is_err(), "Operation should error on TimeZone::Any");
assert!(result
.unwrap_err()
.to_string()
.contains("unconstrained timezone"));
}
#[test]
fn test_cross_timezone_equality() {
use chrono_tz::Tz;
let instant = Utc.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
let ts_utc = TimestampValue::new(instant, TimeZone::Named(Tz::UTC));
let ts_nyc = TimestampValue::new(instant, TimeZone::Named(Tz::America__New_York));
let ts_tokyo = TimestampValue::new(instant, TimeZone::Named(Tz::Asia__Tokyo));
let ts_fixed = TimestampValue::new(
instant,
TimeZone::FixedOffset(FixedOffset::east_opt(5 * 3600).unwrap()),
);
assert_eq!(ts_utc, ts_nyc);
assert_eq!(ts_utc, ts_tokyo);
assert_eq!(ts_nyc, ts_tokyo);
assert_eq!(ts_utc, ts_fixed);
}
#[rstest]
#[case::lt(
|a: &TimestampValue, b: &TimestampValue| a < b,
|a: &DateTime<Utc>, b: &DateTime<Utc>| a < b
)]
#[case::gt(
|a: &TimestampValue, b: &TimestampValue| a > b,
|a: &DateTime<Utc>, b: &DateTime<Utc>| a > b
)]
#[case::le(
|a: &TimestampValue, b: &TimestampValue| a <= b,
|a: &DateTime<Utc>, b: &DateTime<Utc>| a <= b
)]
#[case::ge(
|a: &TimestampValue, b: &TimestampValue| a >= b,
|a: &DateTime<Utc>, b: &DateTime<Utc>| a >= b
)]
fn test_cross_timezone_ordering<F, G>(#[case] ts_op: F, #[case] dt_op: G)
where
F: Fn(&TimestampValue, &TimestampValue) -> bool,
G: Fn(&DateTime<Utc>, &DateTime<Utc>) -> bool,
{
use chrono_tz::Tz;
let earlier = Utc.with_ymd_and_hms(2024, 6, 15, 10, 0, 0).unwrap();
let later = Utc.with_ymd_and_hms(2024, 6, 15, 20, 0, 0).unwrap();
let ts_earlier_nyc = TimestampValue::new(earlier, TimeZone::Named(Tz::America__New_York));
let ts_later_tokyo = TimestampValue::new(later, TimeZone::Named(Tz::Asia__Tokyo));
let ts_earlier_utc = TimestampValue::new(earlier, TimeZone::Named(Tz::UTC));
let ts_later_fixed = TimestampValue::new(
later,
TimeZone::FixedOffset(FixedOffset::west_opt(8 * 3600).unwrap()),
);
assert_eq!(
ts_op(&ts_earlier_nyc, &ts_later_tokyo),
dt_op(&earlier, &later)
);
assert_eq!(
ts_op(&ts_earlier_utc, &ts_later_fixed),
dt_op(&earlier, &later)
);
}
#[test]
fn test_cross_timezone_ordering_with_component_differences() {
use chrono_tz::Tz;
let instant = Utc.with_ymd_and_hms(2024, 1, 1, 5, 0, 0).unwrap();
let ts_utc = TimestampValue::new(instant, TimeZone::Named(Tz::UTC));
let ts_nyc = TimestampValue::new(instant, TimeZone::Named(Tz::America__New_York));
assert_eq!(ts_utc.hour().unwrap(), 5);
assert_eq!(ts_nyc.hour().unwrap(), 0);
assert_eq!(ts_utc, ts_nyc);
assert!(ts_utc <= ts_nyc && ts_utc >= ts_nyc);
assert!(!(ts_utc < ts_nyc) && !(ts_utc > ts_nyc));
}