Merge 'Add partial support for datetime() function' from Preston Thorpe

This PR adds the `datetime` function, with all the support currently
that date/time have for modifiers, and `julianday` function, as well as
some additional modifiers for date/time/datetime.
There are a couple considerations here, I left a couple comments but
essentially there is going to have to be some more work done to track
the state of the expression during the application of modifiers, to
handle a bunch of edge-cases like re-applying the same timezone modifier
to itself, or converting an integer automatically assumed to be
julianday, into epoch, or `ceiling`/`floor` which will determine
relative addition of time in cases like
```
2024-01-31 +1 month = 2024-03-02
```
which was painful enough to get working to begin with.
I couldn't get the `julianday_converter` library to get the exact same
float precision as sqlite, so function is included that matches their
output, for some reason floating point math + `.floor()` would give the
correct result. They seem to 'round' to 8 decimal places, and I was able
to get this to work with the same output as sqlite, except in cases like
`2234.5`, in which case we return `2234.5000000` because of the `fmt`
precision:
```rust
pub fn exec_julianday(time_value: &OwnedValue) -> Result<String> {
    let dt = parse_naive_date_time(time_value);
    match dt {
        // if we did something heinous like: parse::<f64>().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()),
    }
}
```
Suggestions would be appreciated on the float precision issue.

Reviewed-by: Sonny <14060682+sonhmai@users.noreply.github.com>

Closes #600
This commit is contained in:
Pekka Enberg
2025-01-05 20:13:13 +02:00
6 changed files with 963 additions and 155 deletions

View File

@@ -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 |

View File

@@ -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)),

View File

@@ -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();

View File

@@ -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)
}
// 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()));
#[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)
}
}
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()));
}
// 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()));
}
} else {
return OwnedValue::build_text(Rc::new(String::new()));
}
}
OwnedValue::build_text(Rc::new(get_time_from_naive_datetime(dt)))
let formatted = format_dt(*dt, output_type, subsec_requested);
OwnedValue::build_text(Rc::new(formatted))
}
fn apply_modifier(dt: &mut NaiveDateTime, modifier: &str) -> Result<()> {
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()
}
}
}
}
// 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<bool> {
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::<Utc>::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<String> {
let dt = parse_naive_date_time(time_value);
match dt {
// if we did something heinous like: parse::<f64>().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<String> {
let dt = parse_naive_date_time(time_value);
match dt {
@@ -120,6 +328,9 @@ pub fn exec_unixepoch(time_value: &OwnedValue) -> Result<String> {
}
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<NaiveDat
fn get_date_time_from_time_value_integer(value: i64) -> Option<NaiveDateTime> {
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<NaiveDateTime> {
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<NaiveDateTime> {
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<NaiveTime>,
years: i32,
months: i32,
days: i32,
seconds: i32,
},
Ceiling,
Floor,
@@ -318,56 +518,7 @@ fn parse_modifier(modifier: &str) -> Result<Modifier> {
let modifier = modifier.trim().to_lowercase();
match modifier.as_str() {
s if s.ends_with(" days") => Ok(Modifier::Days(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(" minutes") => {
Ok(Modifier::Minutes(parse_modifier_number(&s[..s.len() - 8])?))
}
s if s.ends_with(" seconds") => {
Ok(Modifier::Seconds(parse_modifier_number(&s[..s.len() - 8])?))
}
s if s.ends_with(" months") => Ok(Modifier::Months(
parse_modifier_number(&s[..s.len() - 7])? 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 parts: Vec<&str> = s[1..].split(' ').collect();
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 })
} else {
// try to parse time if error parsing date
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))
}
}
2 => {
let date = parse_modifier_date(parts[0])?;
let time = parse_modifier_time(parts[1])?;
Ok(Modifier::DateTimeOffset {
date,
time: Some(time),
})
}
_ => Err(InvalidModifier(
"Invalid date/time offset format".to_string(),
)),
}
}
// exact matches first
"ceiling" => Ok(Modifier::Ceiling),
"floor" => Ok(Modifier::Floor),
"start of month" => Ok(Modifier::StartOfMonth),
@@ -389,7 +540,76 @@ fn parse_modifier(modifier: &str) -> Result<Modifier> {
"localtime" => Ok(Modifier::Localtime),
"utc" => Ok(Modifier::Utc),
"subsec" | "subsecond" => Ok(Modifier::Subsec),
_ => Err(InvalidModifier(format!("Unknown modifier: {}", modifier))),
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('-') => {
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 => {
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 {
// time values are either 12, 8 or 5 digits
let time = parse_modifier_time(parts[0])?;
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 {
years: sign * (date.year() as i32),
months: sign * (date.month() as i32),
days: sign * date.day() as i32,
seconds: time_delta,
})
}
_ => Err(InvalidModifier(
"Invalid date/time offset format".to_string(),
)),
}
}
_ => 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));
}
}

View File

@@ -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(

View File

@@ -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}
} {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}