diff --git a/COMPAT.md b/COMPAT.md index 3694c3ee3..9ec291e69 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -220,7 +220,7 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html). |-------------|---------|------------------------------| | date() | Yes | partially supports modifiers | | time() | Yes | partially supports modifiers | -| datetime() | No | | +| datetime() | Yes | partially supports modifiers | | julianday() | No | | | unixepoch() | Partial | does not support modifiers | | strftime() | No | | diff --git a/core/function.rs b/core/function.rs index 4bb6a3f9b..4d3f9ee31 100644 --- a/core/function.rs +++ b/core/function.rs @@ -102,6 +102,7 @@ pub enum ScalarFunc { Soundex, Date, Time, + DateTime, Typeof, Unicode, Quote, @@ -157,6 +158,7 @@ impl Display for ScalarFunc { Self::ZeroBlob => "zeroblob".to_string(), Self::LastInsertRowid => "last_insert_rowid".to_string(), Self::Replace => "replace".to_string(), + Self::DateTime => "datetime".to_string(), }; write!(f, "{}", str) } @@ -344,6 +346,7 @@ impl Func { "substring" => Ok(Self::Scalar(ScalarFunc::Substring)), "date" => Ok(Self::Scalar(ScalarFunc::Date)), "time" => Ok(Self::Scalar(ScalarFunc::Time)), + "datetime" => Ok(Self::Scalar(ScalarFunc::DateTime)), "typeof" => Ok(Self::Scalar(ScalarFunc::Typeof)), "last_insert_rowid" => Ok(Self::Scalar(ScalarFunc::LastInsertRowid)), "unicode" => Ok(Self::Scalar(ScalarFunc::Unicode)), diff --git a/core/translate/expr.rs b/core/translate/expr.rs index bd8d9c94e..3b0a6ea97 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -1344,7 +1344,7 @@ pub fn translate_expr( }); Ok(target_register) } - ScalarFunc::Date => { + ScalarFunc::Date | ScalarFunc::DateTime => { if let Some(args) = args { for arg in args.iter() { // register containing result of each argument expression diff --git a/core/vdbe/datetime.rs b/core/vdbe/datetime.rs index 7b9e49fd6..233bc1ab6 100644 --- a/core/vdbe/datetime.rs +++ b/core/vdbe/datetime.rs @@ -1,62 +1,102 @@ -use chrono::{ - DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, TimeZone, Timelike, Utc, -}; -use std::rc::Rc; - use crate::types::OwnedValue; use crate::LimboError::InvalidModifier; use crate::Result; +use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, Timelike, Utc}; +use julian_day_converter::JulianDay; +use std::rc::Rc; -/// Implementation of the date() SQL function. +/// Executi n of date/time/datetime with support for all modifiers. pub fn exec_date(values: &[OwnedValue]) -> OwnedValue { - let maybe_dt = match values.first() { - None => parse_naive_date_time(&OwnedValue::build_text(Rc::new("now".to_string()))), - Some(value) => parse_naive_date_time(value), - }; - // early return, no need to look at modifiers if result invalid - if maybe_dt.is_none() { - return OwnedValue::build_text(Rc::new(String::new())); + exec_datetime(values, DateTimeOutput::Date) +} + +pub fn exec_time(values: &[OwnedValue]) -> OwnedValue { + exec_datetime(values, DateTimeOutput::Time) +} + +pub fn exec_datetime_full(values: &[OwnedValue]) -> OwnedValue { + exec_datetime(values, DateTimeOutput::DateTime) +} + +enum DateTimeOutput { + Date, + Time, + DateTime, +} + +fn exec_datetime(values: &[OwnedValue], output_type: DateTimeOutput) -> OwnedValue { + if values.is_empty() { + return OwnedValue::build_text(Rc::new( + parse_naive_date_time(&OwnedValue::build_text(Rc::new("now".to_string()))) + .unwrap() + .format(match output_type { + DateTimeOutput::DateTime => "%Y-%m-%d %H:%M:%S", + DateTimeOutput::Time => "%H:%M:%S", + DateTimeOutput::Date => "%Y-%m-%d", + }) + .to_string(), + )); + } + if let Some(mut dt) = parse_naive_date_time(&values[0]) { + // if successful, treat subsequent entries as modifiers + log::debug!("first argument valid naivedatetime: {:?}", values[0]); + modify_dt(&mut dt, &values[1..], output_type) + } else { + // if the first argument is NOT a valid date/time, treat the entire set of values as modifiers. + log::debug!("first argument not valid naivedatetime: {:?}", values[0]); + let mut dt = + parse_naive_date_time(&OwnedValue::build_text(Rc::new("now".to_string()))).unwrap(); + modify_dt(&mut dt, values, output_type) } +} - // apply modifiers if result is valid - let mut dt = maybe_dt.unwrap(); - for modifier in values.iter().skip(1) { - if let OwnedValue::Text(modifier_str) = modifier { - if apply_modifier(&mut dt, &modifier_str.value).is_err() { +fn modify_dt( + dt: &mut NaiveDateTime, + mods: &[OwnedValue], + output_type: DateTimeOutput, +) -> OwnedValue { + let mut subsec_requested = false; + + for modifier in mods { + if let OwnedValue::Text(ref text_rc) = modifier { + let raw_mod_str = text_rc.value.trim(); + let lower = raw_mod_str.to_lowercase(); + if lower == "subsec" || lower == "subsecond" { + subsec_requested = true; + continue; + } + if apply_modifier(dt, raw_mod_str).is_err() { return OwnedValue::build_text(Rc::new(String::new())); } } else { return OwnedValue::build_text(Rc::new(String::new())); } } - - OwnedValue::build_text(Rc::new(get_date_from_naive_datetime(dt))) -} - -/// Implementation of the time() SQL function. -pub fn exec_time(time_value: &[OwnedValue]) -> OwnedValue { - let maybe_dt = match time_value.first() { - None => parse_naive_date_time(&OwnedValue::build_text(Rc::new("now".to_string()))), - Some(value) => parse_naive_date_time(value), - }; - // early return, no need to look at modifiers if result invalid - if maybe_dt.is_none() { + if is_leap_second(dt) || *dt > get_max_datetime_exclusive() { return OwnedValue::build_text(Rc::new(String::new())); } + let formatted = format_dt(*dt, output_type, subsec_requested); + OwnedValue::build_text(Rc::new(formatted)) +} - // apply modifiers if result is valid - let mut dt = maybe_dt.unwrap(); - for modifier in time_value.iter().skip(1) { - if let OwnedValue::Text(modifier_str) = modifier { - if apply_modifier(&mut dt, &modifier_str.value).is_err() { - return OwnedValue::build_text(Rc::new(String::new())); +fn format_dt(dt: NaiveDateTime, output_type: DateTimeOutput, subsec: bool) -> String { + match output_type { + DateTimeOutput::Date => dt.format("%Y-%m-%d").to_string(), + DateTimeOutput::Time => { + if subsec { + dt.format("%H:%M:%S%.3f").to_string() + } else { + dt.format("%H:%M:%S").to_string() + } + } + DateTimeOutput::DateTime => { + if subsec { + dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string() + } else { + dt.format("%Y-%m-%d %H:%M:%S").to_string() } - } else { - return OwnedValue::build_text(Rc::new(String::new())); } } - - OwnedValue::build_text(Rc::new(get_time_from_naive_datetime(dt))) } fn apply_modifier(dt: &mut NaiveDateTime, modifier: &str) -> Result<()> { @@ -67,8 +107,15 @@ fn apply_modifier(dt: &mut NaiveDateTime, modifier: &str) -> Result<()> { Modifier::Hours(hours) => *dt += TimeDelta::hours(hours), Modifier::Minutes(minutes) => *dt += TimeDelta::minutes(minutes), Modifier::Seconds(seconds) => *dt += TimeDelta::seconds(seconds), - Modifier::Months(_months) => todo!(), - Modifier::Years(_years) => todo!(), + Modifier::Months(m) => { + // Convert months to years + leftover months + let years = m / 12; + let leftover = m % 12; + add_years_and_months(dt, years, leftover)?; + } + Modifier::Years(y) => { + add_years_and_months(dt, y, 0)?; + } Modifier::TimeOffset(offset) => *dt += offset, Modifier::DateOffset { years, @@ -80,10 +127,34 @@ fn apply_modifier(dt: &mut NaiveDateTime, modifier: &str) -> Result<()> { .ok_or_else(|| InvalidModifier("Invalid date offset".to_string()))?; *dt += TimeDelta::days(days as i64); } - Modifier::DateTimeOffset { date: _, time: _ } => todo!(), - Modifier::Ceiling => todo!(), - Modifier::Floor => todo!(), - Modifier::StartOfMonth => todo!(), + Modifier::DateTimeOffset { date, time } => { + let year_diff = date.year() - dt.date().year(); + let month_diff = date.month() as i32 - dt.date().month() as i32; + let day_diff = date.day() as i64 - dt.date().day() as i64; + + add_years_and_months(dt, year_diff, month_diff)?; + *dt += TimeDelta::days(day_diff); + + if let Some(t) = time { + // Convert dt.time() to seconds, new time to seconds, offset by their difference + let old_secs = dt.time().num_seconds_from_midnight() as i64; + let new_secs = t.num_seconds_from_midnight() as i64; + *dt += TimeDelta::seconds(new_secs - old_secs); + } + } + Modifier::Ceiling => { + if dt.nanosecond() > 0 { + *dt += TimeDelta::seconds(1); + *dt = dt.with_nanosecond(0).unwrap(); + } + } + Modifier::Floor => *dt = dt.with_nanosecond(0).unwrap(), + Modifier::StartOfMonth => { + *dt = NaiveDate::from_ymd_opt(dt.year(), dt.month(), 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + } Modifier::StartOfYear => { *dt = NaiveDate::from_ymd_opt(dt.year(), 1, 1) .unwrap() @@ -93,24 +164,145 @@ fn apply_modifier(dt: &mut NaiveDateTime, modifier: &str) -> Result<()> { Modifier::StartOfDay => { *dt = dt.date().and_hms_opt(0, 0, 0).unwrap(); } - Modifier::Weekday(_day) => todo!(), - Modifier::UnixEpoch => todo!(), - Modifier::JulianDay => todo!(), - Modifier::Auto => todo!(), + Modifier::Weekday(day) => { + let current_day = dt.weekday().num_days_from_sunday(); + let target_day = day; + let days_to_add = (target_day + 7 - current_day) % 7; + *dt += TimeDelta::days(days_to_add as i64); + } + Modifier::UnixEpoch => { + let timestamp = dt.and_utc().timestamp(); + *dt = DateTime::from_timestamp(timestamp, 0) + .ok_or(InvalidModifier("Invalid Unix epoch".to_string()))? + .naive_utc(); + } + Modifier::JulianDay => { + let as_float = dt.to_jd(); + // we already assume valid integers are jd, so to prevent + // something like datetime(2460082.5, 'julianday') failing, + // make sure it's not already in the valid range + if !is_julian_day_value(as_float) { + *dt = julian_day_converter::julian_day_to_datetime(as_float) + .map_err(|_| InvalidModifier("Invalid Julian day".to_string()))?; + } + } + Modifier::Auto => { + if dt.and_utc().timestamp() > 0 { + *dt = DateTime::from_timestamp(dt.and_utc().timestamp(), 0) + .ok_or_else(|| InvalidModifier("Invalid Auto format".to_string()))? + .naive_utc(); + } + } Modifier::Localtime => { let utc_dt = DateTime::::from_naive_utc_and_offset(*dt, Utc); *dt = utc_dt.with_timezone(&chrono::Local).naive_local(); } Modifier::Utc => { - let local_dt = chrono::Local.from_local_datetime(dt).unwrap(); - *dt = local_dt.with_timezone(&Utc).naive_utc(); + *dt = dt.and_utc().naive_utc(); } - Modifier::Subsec => todo!(), + Modifier::Subsec => *dt = dt.with_nanosecond(dt.nanosecond()).unwrap(), + } + + Ok(()) +} + +#[inline] +fn is_julian_day_value(value: f64) -> bool { + (0.0..5373484.5).contains(&value) +} + +fn add_years_and_months(dt: &mut NaiveDateTime, years: i32, months: i32) -> Result<()> { + add_whole_years(dt, years)?; + add_months_in_increments(dt, months)?; + Ok(()) +} + +fn add_whole_years(dt: &mut NaiveDateTime, years: i32) -> Result<()> { + if years == 0 { + return Ok(()); + } + let target_year = dt.year() + years; + let (m, d, hh, mm, ss) = (dt.month(), dt.day(), dt.hour(), dt.minute(), dt.second()); + + // attempt same (month, day) in new year + if let Some(date) = NaiveDate::from_ymd_opt(target_year, m, d) { + *dt = date + .and_hms_opt(hh, mm, ss) + .ok_or_else(|| InvalidModifier("Invalid Auto format".to_string()))?; + return Ok(()); } + // if invalid: compute overflow days + let last_day_in_feb = last_day_in_month(target_year, m); + if d > last_day_in_feb { + // leftover = d - last_day_in_feb + let leftover = d - last_day_in_feb; + // base date is last_day_in_feb + let base_date = NaiveDate::from_ymd_opt(target_year, m, last_day_in_feb) + .ok_or_else(|| InvalidModifier("Invalid Auto format".to_string()))? // should succeed + .and_hms_opt(hh, mm, ss) + .ok_or_else(|| InvalidModifier("Invalid Auto format".to_string()))?; + + *dt = base_date + chrono::Duration::days(leftover as i64); + } else { + // do we fall back here? + } Ok(()) } +fn add_months_in_increments(dt: &mut NaiveDateTime, months: i32) -> Result<()> { + let step = if months >= 0 { 1 } else { -1 }; + for _ in 0..months.abs() { + add_one_month(dt, step)?; + } + Ok(()) +} + +fn add_one_month(dt: &mut NaiveDateTime, step: i32) -> Result<()> { + let (y0, m0, d0) = (dt.year(), dt.month(), dt.day()); + let (hh, mm, ss) = (dt.hour(), dt.minute(), dt.second()); + + // new year & month + let mut new_year = y0; + let mut new_month = m0 as i32 + step; + if new_month > 12 { + new_month -= 12; + new_year += 1; + } else if new_month < 1 { + new_month += 12; + new_year -= 1; + } + + let last_day = last_day_in_month(new_year, new_month as u32); + if d0 <= last_day { + // valid date + *dt = NaiveDate::from_ymd_opt(new_year, new_month as u32, d0) + .ok_or_else(|| InvalidModifier("Invalid Auto format".to_string()))? + .and_hms_opt(hh, mm, ss) + .ok_or_else(|| InvalidModifier("Invalid Auto format".to_string()))?; + } else { + // leftover = d0 - last_day + let leftover = d0 - last_day; + let base_date = NaiveDate::from_ymd_opt(new_year, new_month as u32, last_day) + .ok_or_else(|| InvalidModifier("Invalid Auto format".to_string()))? + .and_hms_opt(hh, mm, ss) + .ok_or_else(|| InvalidModifier("Invalid Auto format".to_string()))?; + + *dt = base_date + chrono::Duration::days(leftover as i64); + } + Ok(()) +} + +fn last_day_in_month(year: i32, month: u32) -> u32 { + // Try day=31,30,... until valid + for day in (28..=31).rev() { + if NaiveDate::from_ymd_opt(year, month, day).is_some() { + return day; + } + } + 28 +} + pub fn exec_unixepoch(time_value: &OwnedValue) -> Result { let dt = parse_naive_date_time(time_value); match dt { @@ -120,6 +312,9 @@ pub fn exec_unixepoch(time_value: &OwnedValue) -> Result { } fn get_unixepoch_from_naive_datetime(value: NaiveDateTime) -> String { + if is_leap_second(&value) { + return String::new(); + } value.and_utc().timestamp().to_string() } @@ -214,7 +409,7 @@ fn get_date_time_from_time_value_integer(value: i64) -> Option { } fn get_date_time_from_time_value_float(value: f64) -> Option { - if value.is_infinite() || value.is_nan() || !(0.0..5373484.5).contains(&value) { + if value.is_infinite() || value.is_nan() || !is_julian_day_value(&value) { return None; } match julian_day_converter::julian_day_to_datetime(value) { @@ -225,25 +420,8 @@ fn get_date_time_from_time_value_float(value: f64) -> Option { fn is_leap_second(dt: &NaiveDateTime) -> bool { // The range from 1,000,000,000 to 1,999,999,999 represents the leap second. - dt.nanosecond() >= 1_000_000_000 && dt.nanosecond() <= 1_999_999_999 -} - -fn get_date_from_naive_datetime(value: NaiveDateTime) -> String { - // NaiveDateTime supports leap seconds, but SQLite does not. - // So we ignore them. - if is_leap_second(&value) || value > get_max_datetime_exclusive() { - return String::new(); - } - value.format("%Y-%m-%d").to_string() -} - -fn get_time_from_naive_datetime(value: NaiveDateTime) -> String { - // NaiveDateTime supports leap seconds, but SQLite does not. - // So we ignore them. - if is_leap_second(&value) || value > get_max_datetime_exclusive() { - return String::new(); - } - value.format("%H:%M:%S").to_string() + dt.second() >= 60 || // Reject invalid seconds + (dt.nanosecond() >= 1_000_000_000 && dt.nanosecond() <= 1_999_999_999) // Nanosecond checks } fn get_max_datetime_exclusive() -> NaiveDateTime { @@ -318,19 +496,55 @@ fn parse_modifier(modifier: &str) -> Result { let modifier = modifier.trim().to_lowercase(); match modifier.as_str() { + // exact matches first + "ceiling" => Ok(Modifier::Ceiling), + "floor" => Ok(Modifier::Floor), + "start of month" => Ok(Modifier::StartOfMonth), + "start of year" => Ok(Modifier::StartOfYear), + "start of day" => Ok(Modifier::StartOfDay), + s if s.starts_with("weekday ") => { + let day = parse_modifier_number(&s[8..])?; + if !(0..=6).contains(&day) { + Err(InvalidModifier( + "Weekday must be between 0 and 6".to_string(), + )) + } else { + Ok(Modifier::Weekday(day as u32)) + } + } + "unixepoch" => Ok(Modifier::UnixEpoch), + "julianday" => Ok(Modifier::JulianDay), + "auto" => Ok(Modifier::Auto), + "localtime" => Ok(Modifier::Localtime), + "utc" => Ok(Modifier::Utc), + "subsec" | "subsecond" => Ok(Modifier::Subsec), + s if s.ends_with(" day") => Ok(Modifier::Days(parse_modifier_number(&s[..s.len() - 4])?)), s if s.ends_with(" days") => Ok(Modifier::Days(parse_modifier_number(&s[..s.len() - 5])?)), + s if s.ends_with(" hour") => Ok(Modifier::Hours(parse_modifier_number(&s[..s.len() - 5])?)), s if s.ends_with(" hours") => { Ok(Modifier::Hours(parse_modifier_number(&s[..s.len() - 6])?)) } + s if s.ends_with(" minute") => { + Ok(Modifier::Minutes(parse_modifier_number(&s[..s.len() - 7])?)) + } s if s.ends_with(" minutes") => { Ok(Modifier::Minutes(parse_modifier_number(&s[..s.len() - 8])?)) } + s if s.ends_with(" second") => { + Ok(Modifier::Seconds(parse_modifier_number(&s[..s.len() - 7])?)) + } s if s.ends_with(" seconds") => { Ok(Modifier::Seconds(parse_modifier_number(&s[..s.len() - 8])?)) } + s if s.ends_with(" month") => Ok(Modifier::Months( + parse_modifier_number(&s[..s.len() - 6])? as i32, + )), s if s.ends_with(" months") => Ok(Modifier::Months( parse_modifier_number(&s[..s.len() - 7])? as i32, )), + s if s.ends_with(" year") => Ok(Modifier::Years( + parse_modifier_number(&s[..s.len() - 5])? as i32 + )), s if s.ends_with(" years") => Ok(Modifier::Years( parse_modifier_number(&s[..s.len() - 6])? as i32, )), @@ -368,27 +582,6 @@ fn parse_modifier(modifier: &str) -> Result { )), } } - "ceiling" => Ok(Modifier::Ceiling), - "floor" => Ok(Modifier::Floor), - "start of month" => Ok(Modifier::StartOfMonth), - "start of year" => Ok(Modifier::StartOfYear), - "start of day" => Ok(Modifier::StartOfDay), - s if s.starts_with("weekday ") => { - let day = parse_modifier_number(&s[8..])?; - if !(0..=6).contains(&day) { - Err(InvalidModifier( - "Weekday must be between 0 and 6".to_string(), - )) - } else { - Ok(Modifier::Weekday(day as u32)) - } - } - "unixepoch" => Ok(Modifier::UnixEpoch), - "julianday" => Ok(Modifier::JulianDay), - "auto" => Ok(Modifier::Auto), - "localtime" => Ok(Modifier::Localtime), - "utc" => Ok(Modifier::Utc), - "subsec" | "subsecond" => Ok(Modifier::Subsec), _ => Err(InvalidModifier(format!("Unknown modifier: {}", modifier))), } } @@ -1176,4 +1369,477 @@ mod tests { apply_modifier(&mut dt, "start of day").unwrap(); assert_eq!(dt, create_datetime(2023, 6, 15, 0, 0, 0)); } + + fn text(value: &str) -> OwnedValue { + OwnedValue::build_text(Rc::new(value.to_string())) + } + + // Basic helper to format NaiveDateTime for comparison + fn format(dt: NaiveDateTime) -> String { + dt.format("%Y-%m-%d %H:%M:%S").to_string() + } + fn weekday_sunday_based(dt: &chrono::NaiveDateTime) -> u32 { + dt.weekday().num_days_from_sunday() + } + + /// Test single modifier: '-1 day' + #[test] + fn test_single_modifier() { + let now = Utc::now().naive_utc(); + let expected = format(now - TimeDelta::days(1)); + let result = exec_datetime(&[text("now"), text("-1 day")], DateTimeOutput::DateTime); + assert_eq!(result, text(&expected)); + } + + /// Test multiple modifiers: '-1 day', '+3 hours' + #[test] + fn test_multiple_modifiers() { + let now = Utc::now().naive_utc(); + let expected = format(now - TimeDelta::days(1) + TimeDelta::hours(3)); + let result = exec_datetime( + &[text("now"), text("-1 day"), text("+3 hours")], + DateTimeOutput::DateTime, + ); + assert_eq!(result, text(&expected)); + } + + /// Test 'subsec' modifier with time output + #[test] + fn test_subsec_modifier() { + let now = Utc::now().naive_utc(); + let expected = now.format("%H:%M:%S%.3f").to_string(); + let result = exec_datetime(&[text("now"), text("subsec")], DateTimeOutput::Time); + assert_eq!(result, text(&expected)); + } + + /// Test 'start of day' with other modifiers + #[test] + fn test_start_of_day_modifier() { + let now = Utc::now().naive_utc(); + let start_of_day = now.date().and_hms_opt(0, 0, 0).unwrap(); + let expected = format(start_of_day - TimeDelta::days(1)); + let result = exec_datetime( + &[text("now"), text("start of day"), text("-1 day")], + DateTimeOutput::DateTime, + ); + assert_eq!(result, text(&expected)); + } + + /// Test 'start of month' with positive offset + #[test] + fn test_start_of_month_modifier() { + let now = Utc::now().naive_utc(); + let start_of_month = NaiveDate::from_ymd_opt(now.year(), now.month(), 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let expected = format(start_of_month + TimeDelta::days(1)); + let result = exec_datetime( + &[text("now"), text("start of month"), text("+1 day")], + DateTimeOutput::DateTime, + ); + assert_eq!(result, text(&expected)); + } + + /// Test 'start of year' with multiple modifiers + #[test] + fn test_start_of_year_modifier() { + let now = Utc::now().naive_utc(); + let start_of_year = NaiveDate::from_ymd_opt(now.year(), 1, 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let expected = format(start_of_year + TimeDelta::days(30) + TimeDelta::hours(5)); + let result = exec_datetime( + &[ + text("now"), + text("start of year"), + text("+30 days"), + text("+5 hours"), + ], + DateTimeOutput::DateTime, + ); + assert_eq!(result, text(&expected)); + } + + /// Test 'localtime' and 'utc' modifiers + #[test] + fn test_localtime_and_utc_modifiers() { + let local = chrono::Local::now().naive_local(); + let expected = format(local); + let result = exec_datetime(&[text("now"), text("localtime")], DateTimeOutput::DateTime); + assert_eq!(result, text(&expected)); + + let utc = Utc::now().naive_utc(); + let expected_utc = format(utc); + let result_utc = exec_datetime(&[text("now"), text("utc")], DateTimeOutput::DateTime); + assert_eq!(result_utc, text(&expected_utc)); + } + + /// Test combined modifiers with 'subsec' and large offsets + #[test] + fn test_combined_modifiers() { + let now = Utc::now().naive_utc(); + let dt = now - TimeDelta::days(1) + + TimeDelta::hours(5) + + TimeDelta::minutes(30) + + TimeDelta::seconds(15); + let expected = dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string(); + let result = exec_datetime( + &[ + text("now"), + text("-1 day"), + text("+5 hours"), + text("+30 minutes"), + text("+15 seconds"), + text("subsec"), + ], + DateTimeOutput::DateTime, + ); + assert_eq!(result, text(&expected)); + } + + // max datetime limit + #[test] + fn test_max_datetime_limit() { + let max = NaiveDate::from_ymd_opt(9999, 12, 31) + .unwrap() + .and_hms_opt(23, 59, 59) + .unwrap(); + let expected = format(max); + let result = exec_datetime(&[text("9999-12-31 23:59:59")], DateTimeOutput::DateTime); + assert_eq!(result, text(&expected)); + } + + // leap second + #[test] + fn test_leap_second_ignored() { + let leap_second = NaiveDate::from_ymd_opt(2024, 6, 30) + .unwrap() + .and_hms_nano_opt(23, 59, 59, 1_500_000_000) // Leap second nanoseconds + .unwrap(); + let expected = String::new(); // SQLite ignores leap seconds + let result = exec_datetime(&[text(&leap_second.to_string())], DateTimeOutput::DateTime); + assert_eq!(result, text(&expected)); + } + + #[test] + fn test_already_on_weekday_no_change() { + // 2023-01-01 is a Sunday => weekday 0 + let mut dt = create_datetime(2023, 1, 1, 12, 0, 0); + apply_modifier(&mut dt, "weekday 0").unwrap(); + assert_eq!(dt, create_datetime(2023, 1, 1, 12, 0, 0)); + assert_eq!(weekday_sunday_based(&dt), 0); + } + + #[test] + fn test_move_forward_if_different() { + // 2023-01-01 is a Sunday => weekday 0 + // "weekday 1" => next Monday => 2023-01-02 + let mut dt = create_datetime(2023, 1, 1, 12, 0, 0); + apply_modifier(&mut dt, "weekday 1").unwrap(); + assert_eq!(dt, create_datetime(2023, 1, 2, 12, 0, 0)); + assert_eq!(weekday_sunday_based(&dt), 1); + + // 2023-01-03 is a Tuesday => weekday 2 + // "weekday 5" => next Friday => 2023-01-06 + let mut dt = create_datetime(2023, 1, 3, 12, 0, 0); + apply_modifier(&mut dt, "weekday 5").unwrap(); + assert_eq!(dt, create_datetime(2023, 1, 6, 12, 0, 0)); + assert_eq!(weekday_sunday_based(&dt), 5); + } + + #[test] + fn test_wrap_around_weekend() { + // 2023-01-06 is a Friday => weekday 5 + // "weekday 0" => next Sunday => 2023-01-08 + let mut dt = create_datetime(2023, 1, 6, 12, 0, 0); + apply_modifier(&mut dt, "weekday 0").unwrap(); + assert_eq!(dt, create_datetime(2023, 1, 8, 12, 0, 0)); + assert_eq!(weekday_sunday_based(&dt), 0); + + // Now confirm that being on Sunday (weekday 0) and asking for "weekday 0" stays put + apply_modifier(&mut dt, "weekday 0").unwrap(); + assert_eq!(dt, create_datetime(2023, 1, 8, 12, 0, 0)); + assert_eq!(weekday_sunday_based(&dt), 0); + } + + #[test] + fn test_same_day_stays_put() { + // 2023-01-05 is a Thursday => weekday 4 + // Asking for weekday 4 => no change + let mut dt = create_datetime(2023, 1, 5, 12, 0, 0); + apply_modifier(&mut dt, "weekday 4").unwrap(); + assert_eq!(dt, create_datetime(2023, 1, 5, 12, 0, 0)); + assert_eq!(weekday_sunday_based(&dt), 4); + } + + #[test] + fn test_already_on_friday_no_change() { + // 2023-01-06 is a Friday => weekday 5 + // Asking for weekday 5 => no change if already on Friday + let mut dt = create_datetime(2023, 1, 6, 12, 0, 0); + apply_modifier(&mut dt, "weekday 5").unwrap(); + assert_eq!(dt, create_datetime(2023, 1, 6, 12, 0, 0)); + assert_eq!(weekday_sunday_based(&dt), 5); + } + + #[test] + fn test_apply_modifier_unixepoch() { + let mut dt = create_datetime(1970, 1, 1, 0, 0, 0); + apply_modifier(&mut dt, "unixepoch").unwrap(); + assert_eq!(dt, create_datetime(1970, 1, 1, 0, 0, 0)); + } + + #[test] + fn test_apply_modifier_julianday() { + let dt = create_datetime(2000, 1, 1, 12, 0, 0); + let julian_day = julian_day_converter::datetime_to_julian_day(&dt.to_string()).unwrap(); + let mut dt_result = NaiveDateTime::default(); + if let Ok(result) = julian_day_converter::julian_day_to_datetime(julian_day) { + dt_result = result; + } + assert_eq!(dt_result, dt); + } + + #[test] + fn test_apply_modifier_ceiling() { + let mut dt = create_datetime(2023, 6, 15, 12, 30, 45); + apply_modifier(&mut dt, "ceiling").unwrap(); + assert_eq!(dt, create_datetime(2023, 6, 15, 12, 30, 45)); + + let mut dt_with_nanos = dt.with_nanosecond(900_000_000).unwrap(); + apply_modifier(&mut dt_with_nanos, "ceiling").unwrap(); + assert_eq!(dt_with_nanos, create_datetime(2023, 6, 15, 12, 30, 46)); + } + + #[test] + fn test_apply_modifier_floor() { + let mut dt = create_datetime(2023, 6, 15, 12, 30, 45); + apply_modifier(&mut dt, "floor").unwrap(); + assert_eq!(dt, create_datetime(2023, 6, 15, 12, 30, 45)); + + let mut dt_with_nanos = dt.with_nanosecond(900_000_000).unwrap(); + apply_modifier(&mut dt_with_nanos, "floor").unwrap(); + assert_eq!(dt_with_nanos, create_datetime(2023, 6, 15, 12, 30, 45)); + } + + #[test] + fn test_apply_modifier_start_of_month() { + let mut dt = create_datetime(2023, 6, 15, 12, 30, 45); + apply_modifier(&mut dt, "start of month").unwrap(); + assert_eq!(dt, create_datetime(2023, 6, 1, 0, 0, 0)); + } + + #[test] + fn test_apply_modifier_auto() { + let mut dt = create_datetime(1970, 1, 1, 0, 0, 0); + apply_modifier(&mut dt, "auto").unwrap(); + assert_eq!(dt, create_datetime(1970, 1, 1, 0, 0, 0)); + } + + #[test] + fn test_apply_modifier_subsec() { + let mut dt = create_datetime(2023, 6, 15, 12, 30, 45); + let dt_with_nanos = dt.with_nanosecond(123_456_789).unwrap(); + dt = dt_with_nanos; + apply_modifier(&mut dt, "subsec").unwrap(); + assert_eq!(dt, dt_with_nanos); + } + #[test] + fn test_apply_modifier_ceiling_at_exact_second() { + // If we're exactly at 12:30:45.000000000, ceiling should do nothing. + let mut dt = create_datetime(2023, 6, 15, 12, 30, 45); + apply_modifier(&mut dt, "ceiling").unwrap(); + assert_eq!(dt, create_datetime(2023, 6, 15, 12, 30, 45)); + } + + #[test] + fn test_apply_modifier_ceiling_above_second() { + // If we’re fractionally above 45s, e.g. 45.123456789, ceiling bumps us to 12:30:46. + let base_dt = create_datetime(2023, 6, 15, 12, 30, 45); + let mut dt_with_nanos = base_dt.with_nanosecond(123_456_789).unwrap(); + apply_modifier(&mut dt_with_nanos, "ceiling").unwrap(); + assert_eq!(dt_with_nanos, create_datetime(2023, 6, 15, 12, 30, 46)); + } + + #[test] + fn test_apply_modifier_ceiling_borderline() { + // If we’re right at 45.999999999, ceiling moves us up to 46. + let base_dt = create_datetime(2023, 6, 15, 12, 30, 45); + let mut dt_with_nanos = base_dt.with_nanosecond(999_999_999).unwrap(); + apply_modifier(&mut dt_with_nanos, "ceiling").unwrap(); + assert_eq!(dt_with_nanos, create_datetime(2023, 6, 15, 12, 30, 46)); + } + + #[test] + fn test_apply_modifier_floor_at_exact_second() { + // If we're exactly at 12:30:45.000000000, floor should do nothing. + let mut dt = create_datetime(2023, 6, 15, 12, 30, 45); + apply_modifier(&mut dt, "floor").unwrap(); + assert_eq!(dt, create_datetime(2023, 6, 15, 12, 30, 45)); + } + + #[test] + fn test_apply_modifier_floor_above_second() { + // If we’re fractionally above 45s, e.g. 45.900000000, floor truncates to 45. + let base_dt = create_datetime(2023, 6, 15, 12, 30, 45); + let mut dt_with_nanos = base_dt.with_nanosecond(900_000_000).unwrap(); + apply_modifier(&mut dt_with_nanos, "floor").unwrap(); + assert_eq!(dt_with_nanos, create_datetime(2023, 6, 15, 12, 30, 45)); + } + + #[test] + fn test_apply_modifier_start_of_month_basic() { + // Basic check: from mid-month to the 1st at 00:00:00. + let mut dt = create_datetime(2023, 6, 15, 12, 30, 45); + apply_modifier(&mut dt, "start of month").unwrap(); + assert_eq!(dt, create_datetime(2023, 6, 1, 0, 0, 0)); + } + + #[test] + fn test_apply_modifier_start_of_month_already_at_first() { + // If we're already at the start of the month, no change. + let mut dt = create_datetime(2023, 6, 1, 0, 0, 0); + apply_modifier(&mut dt, "start of month").unwrap(); + assert_eq!(dt, create_datetime(2023, 6, 1, 0, 0, 0)); + } + + #[test] + fn test_apply_modifier_start_of_month_edge_case() { + // edge case: month boundary. 2023-07-31 -> start of July. + let mut dt = create_datetime(2023, 7, 31, 23, 59, 59); + apply_modifier(&mut dt, "start of month").unwrap(); + assert_eq!(dt, create_datetime(2023, 7, 1, 0, 0, 0)); + } + + #[test] + fn test_apply_modifier_auto_no_change() { + // If "auto" is effectively a no-op (the logic you intend is unknown, but let's + // assume it does nothing if the date is already valid). + let mut dt = create_datetime(1970, 1, 1, 0, 0, 0); + apply_modifier(&mut dt, "auto").unwrap(); + assert_eq!(dt, create_datetime(1970, 1, 1, 0, 0, 0)); + } + + #[test] + fn test_apply_modifier_auto_custom_logic() { + // If "auto" is supposed to do something special if the datetime is "invalid", + // you can add a scenario for that. For demonstration, we’ll just assume it does nothing. + // Example: + let mut dt = create_datetime(9999, 12, 31, 23, 59, 59); + apply_modifier(&mut dt, "auto").unwrap(); + assert_eq!(dt, create_datetime(9999, 12, 31, 23, 59, 59)); + } + + #[test] + fn test_apply_modifier_subsec_no_change() { + let mut dt = create_datetime(2023, 6, 15, 12, 30, 45); + let dt_with_nanos = dt.with_nanosecond(123_456_789).unwrap(); + dt = dt_with_nanos; + apply_modifier(&mut dt, "subsec").unwrap(); + assert_eq!(dt, dt_with_nanos); + } + + #[test] + fn test_apply_modifier_auto_before_epoch_no_change() { + let mut dt = create_datetime(1969, 12, 31, 23, 59, 59); + apply_modifier(&mut dt, "auto").unwrap(); + // Expect no change because dt is before epoch (timestamp <= 0). + assert_eq!(dt, create_datetime(1969, 12, 31, 23, 59, 59)); + } + + #[test] + fn test_apply_modifier_auto_after_epoch_truncate_to_second() { + let mut dt = create_datetime(1970, 1, 1, 0, 0, 10); + dt = dt.with_nanosecond(500_000_000).unwrap(); // half-second fraction + apply_modifier(&mut dt, "auto").unwrap(); + assert_eq!(dt.second(), 10); + assert_eq!(dt.nanosecond(), 0); + } + + #[test] + fn test_apply_modifier_auto_exact_second_after_epoch() { + let mut dt = create_datetime(1970, 1, 1, 0, 0, 10); + apply_modifier(&mut dt, "auto").unwrap(); + assert_eq!(dt, create_datetime(1970, 1, 1, 0, 0, 10)); + } + + #[test] + fn test_apply_modifier_auto_far_future() { + // ensure we handle large timestamps gracefully + let mut dt = create_datetime(9999, 12, 31, 23, 59, 59) + .with_nanosecond(123_456_789) + .unwrap(); + apply_modifier(&mut dt, "auto").unwrap(); + assert_eq!(dt, create_datetime(9999, 12, 31, 23, 59, 59)); + } + + #[test] + fn test_apply_modifier_subsec_preserves_fractional_seconds() { + let mut dt = create_datetime(2025, 1, 2, 4, 12, 21) + .with_nanosecond(891_000_000) // 891 milliseconds + .unwrap(); + apply_modifier(&mut dt, "subsec").unwrap(); + + let formatted = dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string(); + assert_eq!(formatted, "2025-01-02 04:12:21.891"); + } + + #[test] + fn test_apply_modifier_subsec_no_fractional_seconds() { + let mut dt = create_datetime(2025, 1, 2, 4, 12, 21); + apply_modifier(&mut dt, "subsec").unwrap(); + + let formatted = dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string(); + assert_eq!(formatted, "2025-01-02 04:12:21.000"); + } + + #[test] + fn test_apply_modifier_subsec_truncate_to_milliseconds() { + let mut dt = create_datetime(2025, 1, 2, 4, 12, 21) + .with_nanosecond(891_123_456) + .unwrap(); + apply_modifier(&mut dt, "subsec").unwrap(); + + let formatted = dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string(); + assert_eq!(formatted, "2025-01-02 04:12:21.891"); + } + + #[test] + fn test_invalid_modifiers() { + let mut dt = create_datetime(2023, 6, 15, 12, 30, 45); + + // Test invalid weekday + let result = apply_modifier(&mut dt, "weekday 7"); + assert!(result.is_err()); + + // Test invalid unixepoch + let result = apply_modifier(&mut dt, "unixepoch invalid"); + assert!(result.is_err()); + + // Test invalid julianday + let result = apply_modifier(&mut dt, "julianday invalid"); + assert!(result.is_err()); + + // Test invalid ceiling + let result = apply_modifier(&mut dt, "ceiling invalid"); + assert!(result.is_err()); + + // Test invalid floor + let result = apply_modifier(&mut dt, "floor invalid"); + assert!(result.is_err()); + + // Test invalid start of month + let result = apply_modifier(&mut dt, "start of month invalid"); + assert!(result.is_err()); + + // Test invalid auto + let result = apply_modifier(&mut dt, "auto invalid"); + assert!(result.is_err()); + + // Test invalid subsec + let result = apply_modifier(&mut dt, "subsec invalid"); + assert!(result.is_err()); + } } diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 76e332c2b..d57cfda83 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -41,7 +41,7 @@ use crate::vdbe::insn::Insn; #[cfg(feature = "json")] use crate::{function::JsonFunc, json::get_json, json::json_array, json::json_array_length}; use crate::{Connection, Result, Rows, TransactionState, DATABASE_VERSION}; -use datetime::{exec_date, exec_time, exec_unixepoch}; +use datetime::{exec_date, exec_datetime_full, exec_time, exec_unixepoch}; use insn::{ exec_add, exec_bit_and, exec_bit_not, exec_bit_or, exec_divide, exec_multiply, exec_remainder, exec_subtract, @@ -1529,6 +1529,12 @@ impl Program { exec_time(&state.registers[*start_reg..*start_reg + arg_count]); state.registers[*dest] = result; } + ScalarFunc::DateTime => { + let result = exec_datetime_full( + &state.registers[*start_reg..*start_reg + arg_count], + ); + state.registers[*dest] = result; + } ScalarFunc::UnixEpoch => { if *start_reg == 0 { let unixepoch: String = exec_unixepoch(