diff --git a/COMPAT.md b/COMPAT.md index c30912d53..83888f87c 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -220,12 +220,39 @@ 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 | | -| julianday() | No | | +| datetime() | Yes | partially supports modifiers | +| julianday() | Partial | does not support modifiers | | unixepoch() | Partial | does not support modifiers | | strftime() | No | | | timediff() | No | | +### Date and Time Modifiers +| Modifier | Status| Comment | +|----------------|-------|---------------------------------| +| Days | Yes | | +| Hours | Yes | | +| Minutes | Yes | | +| Seconds | Yes | | +| Months | Yes | | +| Years | Yes | | +| TimeOffset | Yes | | +| DateOffset | Yes | | +| DateTimeOffset | Yes | | +| Ceiling | No | | +| Floor | No | | +| StartOfMonth | Yes | | +| StartOfYear | Yes | | +| StartOfDay | Yes | | +| Weekday(N) | Yes | | +| Auto | No | | +| UnixEpoch | No | | +| JulianDay | No | | +| Localtime |Partial| requires fixes to avoid double conversions.| +| Utc |Partial| requires fixes to avoid double conversions.| +| Subsec | Yes | | + + + ### JSON functions | Function | Status | Comment | diff --git a/core/function.rs b/core/function.rs index 9813b8168..7385b237f 100644 --- a/core/function.rs +++ b/core/function.rs @@ -106,11 +106,13 @@ pub enum ScalarFunc { Date, Time, TotalChanges, + DateTime, Typeof, Unicode, Quote, SqliteVersion, UnixEpoch, + JulianDay, Hex, Unhex, ZeroBlob, @@ -157,12 +159,14 @@ impl Display for ScalarFunc { Self::Unicode => "unicode".to_string(), Self::Quote => "quote".to_string(), Self::SqliteVersion => "sqlite_version".to_string(), + Self::JulianDay => "julianday".to_string(), Self::UnixEpoch => "unixepoch".to_string(), Self::Hex => "hex".to_string(), Self::Unhex => "unhex".to_string(), 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) } @@ -352,6 +356,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)), @@ -367,6 +372,7 @@ impl Func { #[cfg(feature = "json")] "json_extract" => Ok(Func::Json(JsonFunc::JsonExtract)), "unixepoch" => Ok(Self::Scalar(ScalarFunc::UnixEpoch)), + "julianday" => Ok(Self::Scalar(ScalarFunc::JulianDay)), "hex" => Ok(Self::Scalar(ScalarFunc::Hex)), "unhex" => Ok(Self::Scalar(ScalarFunc::Unhex)), "zeroblob" => Ok(Self::Scalar(ScalarFunc::ZeroBlob)), diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 19a7c2c8f..c55bad5da 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -1239,7 +1239,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 @@ -1334,11 +1334,11 @@ pub fn translate_expr( }); Ok(target_register) } - ScalarFunc::UnixEpoch => { + ScalarFunc::UnixEpoch | ScalarFunc::JulianDay => { let mut start_reg = 0; match args { Some(args) if args.len() > 1 => { - crate::bail_parse_error!("epoch function with > 1 arguments. Modifiers are not yet supported."); + crate::bail_parse_error!("epoch or julianday function with > 1 arguments. Modifiers are not yet supported."); } Some(args) if args.len() == 1 => { let arg_reg = program.alloc_register(); diff --git a/core/vdbe/datetime.rs b/core/vdbe/datetime.rs index 7b9e49fd6..37e66ae69 100644 --- a/core/vdbe/datetime.rs +++ b/core/vdbe/datetime.rs @@ -1,65 +1,106 @@ +use crate::types::OwnedValue; +use crate::LimboError::InvalidModifier; +use crate::Result; 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; - -/// Implementation of the date() SQL function. +/// Execution of date/time/datetime functions +#[inline(always)] 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) +} + +#[inline(always)] +pub fn exec_time(values: &[OwnedValue]) -> OwnedValue { + exec_datetime(values, DateTimeOutput::Time) +} + +#[inline(always)] +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 + 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. + let mut dt = chrono::Local::now().to_utc().naive_utc(); + 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() { - return OwnedValue::build_text(Rc::new(String::new())); +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 { + // TODO: to prevent double conversion and properly support 'utc'/'localtime', we also + // need to keep track of the current timezone and apply it to the modifier. + match apply_modifier(dt, &text_rc.value) { + Ok(true) => subsec_requested = true, + Ok(false) => {} + 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<()> { +// to prevent stripping the modifier string and comparing multiple times, this returns +// whether the modifier was a subsec modifier because it impacts the format string +fn apply_modifier(dt: &mut NaiveDateTime, modifier: &str) -> Result { let parsed_modifier = parse_modifier(modifier)?; match parsed_modifier { @@ -67,8 +108,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 +128,24 @@ 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::DateTimeOffset { + years, + months, + days, + seconds, + } => { + add_years_and_months(dt, years, months)?; + *dt += chrono::Duration::days(days as i64); + *dt += chrono::Duration::seconds(seconds.into()); + } Modifier::Ceiling => todo!(), Modifier::Floor => todo!(), - Modifier::StartOfMonth => todo!(), + 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 +155,170 @@ 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::Auto => todo!(), // Will require storing info about the original arg passed when + Modifier::UnixEpoch => todo!(), // applying modifiers. All numbers passed to date/time/dt are + Modifier::JulianDay => todo!(), // assumed to be julianday, so adding these now is redundant Modifier::Localtime => { let utc_dt = DateTime::::from_naive_utc_and_offset(*dt, Utc); *dt = utc_dt.with_timezone(&chrono::Local).naive_local(); } Modifier::Utc => { + // TODO: handle datetime('now', 'utc') no-op let local_dt = chrono::Local.from_local_datetime(dt).unwrap(); *dt = local_dt.with_timezone(&Utc).naive_utc(); } - Modifier::Subsec => todo!(), + Modifier::Subsec => { + *dt = dt.with_nanosecond(dt.nanosecond()).unwrap(); + return Ok(true); + } + } + + Ok(false) +} + +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 datetime 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 datetime format".to_string()))? + .and_hms_opt(hh, mm, ss) + .ok_or_else(|| InvalidModifier("Invalid time 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(()) +} + +// sqlite resolves any ambiguity between advancing months by using the 'ceiling' +// value, computing overflow days and advancing to the next valid date +// e.g. 2024-01-31 + 1 month = 2024-03-02 +// +// the modifiers 'ceiling' and 'floor' will determine behavior, so we'll need to eagerly +// evaluate modifiers in the future to support those, and 'julianday'/'unixepoch' +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()); + + 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 { + 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(()) +} + +#[inline(always)] +fn last_day_in_month(year: i32, month: u32) -> u32 { + for day in (28..=31).rev() { + if NaiveDate::from_ymd_opt(year, month, day).is_some() { + return day; + } + } + 28 +} + +pub fn exec_julianday(time_value: &OwnedValue) -> Result { + let dt = parse_naive_date_time(time_value); + match dt { + // if we did something heinous like: parse::().unwrap().to_string() + // that would solve the precision issue, but dear lord... + Some(dt) => Ok(format!("{:.1$}", to_julian_day_exact(&dt), 8)), + None => Ok(String::new()), + } +} + +fn to_julian_day_exact(dt: &NaiveDateTime) -> f64 { + let year = dt.year(); + let month = dt.month() as i32; + let day = dt.day() as i32; + let (adjusted_year, adjusted_month) = if month <= 2 { + (year - 1, month + 12) + } else { + (year, month) + }; + + let a = adjusted_year / 100; + let b = 2 - a + a / 4; + let jd_days = (365.25 * ((adjusted_year + 4716) as f64)).floor() + + (30.6001 * ((adjusted_month + 1) as f64)).floor() + + (day as f64) + + (b as f64) + - 1524.5; + + let seconds = dt.hour() as f64 * 3600.0 + + dt.minute() as f64 * 60.0 + + dt.second() as f64 + + (dt.nanosecond() as f64) / 1_000_000_000.0; + + let jd_fraction = seconds / 86400.0; + jd_days + jd_fraction +} + pub fn exec_unixepoch(time_value: &OwnedValue) -> Result { let dt = parse_naive_date_time(time_value); match dt { @@ -120,6 +328,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() } @@ -209,12 +420,17 @@ fn parse_datetime_with_optional_tz(value: &str, format: &str) -> Option Option { i32::try_from(value).map_or_else( |_| None, - |value| get_date_time_from_time_value_float(value as f64), + |value| { + if value.is_negative() || !is_julian_day_value(value as f64) { + return None; + } + get_date_time_from_time_value_float(value as f64) + }, ) } 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 +441,7 @@ 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() == 59 && dt.nanosecond() > 999_999_999 } fn get_max_datetime_exclusive() -> NaiveDateTime { @@ -271,8 +469,10 @@ enum Modifier { days: i32, }, DateTimeOffset { - date: NaiveDate, - time: Option, + years: i32, + months: i32, + days: i32, + seconds: i32, }, Ceiling, Floor, @@ -318,49 +518,88 @@ 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, )), s if s.starts_with('+') || s.starts_with('-') => { - // Parse as DateOffset or DateTimeOffset + let sign = if s.starts_with('-') { -1 } else { 1 }; let parts: Vec<&str> = s[1..].split(' ').collect(); + let digits_in_date = 10; match parts.len() { 1 => { - // first part can be either date ±YYYY-MM-DD or 3 types of time modifiers - let date = parse_modifier_date(parts[0]); - if let Ok(date) = date { - Ok(Modifier::DateTimeOffset { date, time: None }) + if parts[0].len() == digits_in_date { + let date = parse_modifier_date(parts[0])?; + Ok(Modifier::DateOffset { + years: sign * date.year() as i32, + months: sign * date.month() as i32, + days: sign * date.day() as i32, + }) } else { - // try to parse time if error parsing date + // time values are either 12, 8 or 5 digits let time = parse_modifier_time(parts[0])?; - // TODO handle nanoseconds - let time_delta = if s.starts_with('-') { - TimeDelta::seconds(-(time.num_seconds_from_midnight() as i64)) - } else { - TimeDelta::seconds(time.num_seconds_from_midnight() as i64) - }; - Ok(Modifier::TimeOffset(time_delta)) + let time_delta = (sign * (time.num_seconds_from_midnight() as i32)) as i32; + Ok(Modifier::TimeOffset(TimeDelta::seconds(time_delta.into()))) } } 2 => { let date = parse_modifier_date(parts[0])?; let time = parse_modifier_time(parts[1])?; + // Convert time to total seconds (with sign) + let time_delta = sign * (time.num_seconds_from_midnight() as i32); Ok(Modifier::DateTimeOffset { - date, - time: Some(time), + years: sign * (date.year() as i32), + months: sign * (date.month() as i32), + days: sign * date.day() as i32, + seconds: time_delta, }) } _ => Err(InvalidModifier( @@ -368,28 +607,9 @@ 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))), + _ => Err(InvalidModifier( + "Invalid date/time offset format".to_string(), + )), } } @@ -971,39 +1191,42 @@ mod tests { #[test] fn test_parse_date_offset() { - let expected_date = NaiveDate::from_ymd_opt(2023, 5, 15).unwrap(); assert_eq!( parse_modifier("+2023-05-15").unwrap(), - Modifier::DateTimeOffset { - date: expected_date, - time: None, + Modifier::DateOffset { + years: 2023, + months: 5, + days: 15, } ); assert_eq!( parse_modifier("-2023-05-15").unwrap(), - Modifier::DateTimeOffset { - date: expected_date, - time: None, + Modifier::DateOffset { + years: -2023, + months: -5, + days: -15, } ); } #[test] fn test_parse_date_time_offset() { - let expected_date = NaiveDate::from_ymd_opt(2023, 5, 15).unwrap(); - let expected_time = NaiveTime::from_hms_opt(14, 30, 0).unwrap(); assert_eq!( parse_modifier("+2023-05-15 14:30").unwrap(), Modifier::DateTimeOffset { - date: expected_date, - time: Some(expected_time), + years: 2023, + months: 5, + days: 15, + seconds: (14 * 60 + 30) * 60, } ); assert_eq!( - parse_modifier("-2023-05-15 14:30").unwrap(), + parse_modifier("-0001-05-15 14:30").unwrap(), Modifier::DateTimeOffset { - date: expected_date, - time: Some(expected_time), + years: -1, + months: -5, + days: -15, + seconds: -((14 * 60 + 30) * 60), } ); } @@ -1143,23 +1366,22 @@ mod tests { } #[test] - #[ignore] // enable when implemented this modifier fn test_apply_modifier_date_time_offset() { let mut dt = setup_datetime(); - apply_modifier(&mut dt, "+01-01-01 01:01").unwrap(); + apply_modifier(&mut dt, "+0001-01-01 01:01").unwrap(); assert_eq!(dt, create_datetime(2024, 7, 16, 13, 31, 45)); dt = setup_datetime(); - apply_modifier(&mut dt, "-01-01-01 01:01").unwrap(); + apply_modifier(&mut dt, "-0001-01-01 01:01").unwrap(); assert_eq!(dt, create_datetime(2022, 5, 14, 11, 29, 45)); // Test with larger offsets dt = setup_datetime(); - apply_modifier(&mut dt, "+02-03-04 05:06").unwrap(); + apply_modifier(&mut dt, "+0002-03-04 05:06").unwrap(); assert_eq!(dt, create_datetime(2025, 9, 19, 17, 36, 45)); dt = setup_datetime(); - apply_modifier(&mut dt, "-02-03-04 05:06").unwrap(); + apply_modifier(&mut dt, "-0002-03-04 05:06").unwrap(); assert_eq!(dt, create_datetime(2021, 3, 11, 7, 24, 45)); } @@ -1176,4 +1398,318 @@ 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] + 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] + 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] + 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] + 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] + 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] + 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(&local.to_string()), text("utc")], + DateTimeOutput::DateTime, + ); + assert_eq!(result_utc, text(&expected_utc)); + } + + #[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)); + } + + #[test] + fn test_max_datetime_limit() { + // 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) + .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_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_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_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_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_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_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_is_leap_second() { + let dt = DateTime::from_timestamp(1483228799, 999_999_999) + .unwrap() + .naive_utc(); + assert!(!is_leap_second(&dt)); + + let dt = DateTime::from_timestamp(1483228799, 1_500_000_000) + .unwrap() + .naive_utc(); + assert!(is_leap_second(&dt)); + } } diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 63e7cae84..a0b42abb6 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -44,7 +44,7 @@ use crate::{ json::json_extract, }; use crate::{Connection, Result, Rows, TransactionState, DATABASE_VERSION}; -use datetime::{exec_date, exec_time, exec_unixepoch}; +use datetime::{exec_date, exec_datetime_full, exec_julianday, 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, @@ -1558,6 +1558,36 @@ impl Program { let total_changes = res.get(); state.registers[*dest] = OwnedValue::Integer(total_changes); } + ScalarFunc::DateTime => { + let result = exec_datetime_full( + &state.registers[*start_reg..*start_reg + arg_count], + ); + state.registers[*dest] = result; + } + ScalarFunc::JulianDay => { + if *start_reg == 0 { + let julianday: String = exec_julianday( + &OwnedValue::build_text(Rc::new("now".to_string())), + )?; + state.registers[*dest] = + OwnedValue::build_text(Rc::new(julianday)); + } else { + let datetime_value = &state.registers[*start_reg]; + let julianday = exec_julianday(datetime_value); + match julianday { + Ok(time) => { + state.registers[*dest] = + OwnedValue::build_text(Rc::new(time)) + } + Err(e) => { + return Err(LimboError::ParseError(format!( + "Error encountered while parsing datetime value: {}", + e + ))); + } + } + } + } ScalarFunc::UnixEpoch => { if *start_reg == 0 { let unixepoch: String = exec_unixepoch( diff --git a/testing/scalar-functions-datetime.test b/testing/scalar-functions-datetime.test index a721dbd18..8831f723e 100755 --- a/testing/scalar-functions-datetime.test +++ b/testing/scalar-functions-datetime.test @@ -233,4 +233,213 @@ do_execsql_test unixepoch-at-start-of-time { do_execsql_test unixepoch-at-millisecond-precision-input-produces-seconds-precision-output { SELECT unixepoch('2022-01-27 12:59:28.052'); -} {1643288368} \ No newline at end of file +} {1643288368} + +do_execsql_test date-with-modifier-start-of-day { + SELECT date('2023-05-18 15:30:45', 'start of day'); +} {2023-05-18} + +do_execsql_test date-with-modifier-start-of-month { + SELECT date('2023-05-18', 'start of month'); +} {2023-05-01} + +do_execsql_test date-with-modifier-start-of-year { + SELECT date('2023-05-18', 'start of year'); +} {2023-01-01} + +do_execsql_test date-with-modifier-add-months { + SELECT date('2023-05-18', '+2 months'); +} {2023-07-18} + +do_execsql_test date-with-modifier-subtract-months { + SELECT date('2023-05-18', '-3 months'); +} {2023-02-18} + +do_execsql_test date-with-modifier-add-years { + SELECT date('2023-05-18', '+1 year'); +} {2024-05-18} + +do_execsql_test date-with-modifier-subtract-years { + SELECT date('2023-05-18', '-2 years'); +} {2021-05-18} + +do_execsql_test date-with-modifier-weekday { + SELECT date('2023-05-18', 'weekday 0'); +} {2023-05-21} + +do_execsql_test date-with-multiple-modifiers { + SELECT date('2023-05-18', '+1 month', '-10 days', 'start of year'); +} {2023-01-01} + +do_execsql_test date-with-subsec { + SELECT date('2023-05-18 15:30:45.123', 'subsec'); +} {2023-05-18} + +do_execsql_test time-with-modifier-add-hours { + SELECT time('2023-05-18 15:30:45', '+5 hours'); +} {20:30:45} + +do_execsql_test time-with-modifier-subtract-hours { + SELECT time('2023-05-18 15:30:45', '-2 hours'); +} {13:30:45} + +do_execsql_test time-with-modifier-add-minutes { + SELECT time('2023-05-18 15:30:45', '+45 minutes'); +} {16:15:45} + +do_execsql_test time-with-modifier-subtract-seconds { + SELECT time('2023-05-18 15:30:45', '-50 seconds'); +} {15:29:55} + +do_execsql_test time-with-subsec { + SELECT time('2023-05-18 15:30:45.123', 'subsec'); +} {15:30:45.123} + +do_execsql_test time-with-modifier-add { + SELECT time('15:30:45', '+15:30:15'); +} {{07:01:00}} + +do_execsql_test time-with-modifier-sub { + SELECT time('15:30:45', '-15:30:15'); +} {{00:00:30}} + +do_execsql_test date-with-modifier-add-months { + SELECT date('2023-01-31', '+1 month'); +} {2023-03-03} + +do_execsql_test date-with-modifier-subtract-months { + SELECT date('2023-03-31', '-1 month'); +} {2023-03-03} + +do_execsql_test date-with-modifier-add-months-large { + SELECT date('2023-01-31', '+13 months'); +} {2024-03-02} + +do_execsql_test date-with-modifier-subtract-months-large { + SELECT date('2023-01-31', '-13 months'); +} {2021-12-31} + +do_execsql_test date-with-modifier-february-leap-year { + SELECT date('2020-02-29', '+12 months'); +} {2021-03-01} + +do_execsql_test date-with-modifier-february-non-leap-year { + SELECT date('2019-02-28', '+12 months'); +} {2020-02-28} + +do_execsql_test date-with-modifier-invalid-date { + SELECT date('2023-02-15 15:30:45', '-0001-01-01 00:00'); +} {2022-01-14} + +do_execsql_test date-with-modifier-date { + SELECT date('2023-02-15 15:30:45', '+0001-01-01'); +} {2024-03-16} + +do_execsql_test datetime-with-modifier-datetime-pos { + SELECT datetime('2023-02-15 15:30:45', '+0001-01-01 15:30'); +} {{2024-03-17 07:00:45}} + +do_execsql_test datetime-with-modifier-datetime-neg { + SELECT datetime('2023-02-15 15:30:45', '+0001-01-01 15:30'); +} {{2024-03-17 07:00:45}} + +do_execsql_test datetime-with-modifier-datetime-large { + SELECT datetime('2023-02-15 15:30:45', '+7777-10-10 23:59'); +} {{9800-12-26 15:29:45}} + +do_execsql_test datetime-with-modifier-datetime-sub-large { + SELECT datetime('2023-02-15 15:30:45', '-2024-10-10 23:59'); +} {{-0002-04-04 15:31:45}} + +do_execsql_test datetime-with-timezone-utc { + SELECT datetime('2023-05-18 15:30:45Z'); +} {{2023-05-18 15:30:45}} + +do_execsql_test datetime-with-modifier-sub { + SELECT datetime('2023-12-12', '-0002-10-10 15:30:45'); +} {{2021-02-01 08:29:15}} + +do_execsql_test datetime-with-modifier-add { + SELECT datetime('2023-12-12', '+0002-10-10 15:30:45'); +} {{2026-10-22 15:30:45}} + +do_execsql_test time-with-multiple-modifiers { + SELECT time('2023-05-18 15:30:45', '+1 hours', '-20 minutes', '+15 seconds', 'subsec'); +} {16:11:00.000} + +do_execsql_test datetime-with-multiple-modifiers { +select datetime('2024-01-31', '+1 month', '+13 hours', '+5 minutes', '+62 seconds'); +} {{2024-03-02 13:06:02}} + +do_execsql_test datetime-with-weekday { + SELECT datetime('2023-05-18', 'weekday 3'); +} {{2023-05-24 00:00:00}} + +do_execsql_test unixepoch-subsec { + SELECT unixepoch('2023-05-18 15:30:45.123'); +} {1684423845} + +do_execsql_test unixepoch-invalid-date { + SELECT unixepoch('not-a-date'); +} {{}} + +do_execsql_test unixepoch-leap-second { + SELECT unixepoch('2015-06-30 23:59:60'); +} {{}} + +do_execsql_test unixepoch-negative-timestamp { + SELECT unixepoch('1969-12-31 23:59:59'); +} {-1} + +do_execsql_test unixepoch-large-date { + SELECT unixepoch('9999-12-31 23:59:59'); +} {253402300799} + +do_execsql_test datetime-with-timezone { + SELECT datetime('2023-05-19 01:30:45+03:00'); +} {{2023-05-18 22:30:45}} + +do_execsql_test julianday-fractional { + SELECT julianday('2023-05-18 15:30:45.123'); +} {2460083.14635559} + +do_execsql_test julianday-fractional-2 { + SELECT julianday('2000-01-01 12:00:00.500'); +} {2451545.00000579} + +do_execsql_test julianday-rounded-up { + SELECT julianday('2023-05-18 15:30:45.129'); +} {2460083.14635566} + +do_execsql_test julianday-with-timezone { + SELECT julianday('2023-05-18 15:30:45+02:00'); +} {2460083.06302083} + +do_execsql_test julianday-fractional-seconds { + SELECT julianday('2023-05-18 15:30:45.123'); +} {2460083.14635559} + +do_execsql_test julianday-time-only { + SELECT julianday('15:30:45'); +} {2451545.14635417} + +# +# TODO: fix precision issue +# +#do_execsql_test julianday-midnight { +# SELECT julianday('2023-05-18 00:00:00'); +#} {2460082.5} + +#do_execsql_test julianday-noon { +# SELECT julianday('2023-05-18 12:00:00'); +#} {2460083.0} + +#do_execsql_test julianday-fractional-zero { +# SELECT julianday('2023-05-18 00:00:00.000'); +#} {2460082.5} + +# same issue as above, we return .5000000 because we are using fmt precision +#do_execsql_test julianday-date-only { +# SELECT julianday('2023-05-18'); +#} {2460082.5} +