Skip to content
76 changes: 50 additions & 26 deletions core/functions/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ where
exec_datetime(values, DateTimeOutput::DateTime)
}

#[inline(always)]
pub fn exec_unixepoch<I, E, V>(values: I) -> Value
where
V: AsValueRef,
E: ExactSizeIterator<Item = V>,
I: IntoIterator<IntoIter = E, Item = V>,
{
exec_datetime(values, DateTimeOutput::UnixEpoch)
}

#[inline(always)]
pub fn exec_strftime<I, E, V>(values: I) -> Value
where
Expand Down Expand Up @@ -70,6 +80,14 @@ enum DateTimeOutput {
// Holds the format string
StrfTime(String),
JuliaDay,
UnixEpoch,
}

// The result of applying a set of modifiers is both a datetime
// and whether the subsec format was requested.
struct DtTransform {
dt: NaiveDateTime,
subsec_requested: bool,
}

fn exec_datetime<I, E, V>(values: I, output_type: DateTimeOutput) -> Value
Expand All @@ -85,17 +103,25 @@ where
}
let mut values = values.peekable();
let first = values.peek().unwrap();
if let Some(mut dt) = parse_naive_date_time(first) {
let apply_res = if let Some(dt) = parse_naive_date_time(first) {
// if successful, treat subsequent entries as modifiers
modify_dt(&mut dt, values.skip(1), output_type)
modify_dt(dt, values.skip(1))
} 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)
let dt = chrono::Local::now().to_utc().naive_utc();
modify_dt(dt, values)
};

match apply_res {
None => Value::build_text(""),
Some(DtTransform {
dt,
subsec_requested,
}) => format_dt(dt, output_type, subsec_requested),
}
}

fn modify_dt<I, E, V>(dt: &mut NaiveDateTime, mods: I, output_type: DateTimeOutput) -> Value
fn modify_dt<I, E, V>(mut dt: NaiveDateTime, mods: I) -> Option<DtTransform>
where
V: AsValueRef,
E: ExactSizeIterator<Item = V>,
Expand All @@ -113,25 +139,28 @@ where
n_floor = 0;
}

match apply_modifier(dt, text_rc.as_str(), &mut n_floor) {
match apply_modifier(&mut dt, text_rc.as_str(), &mut n_floor) {
Ok(true) => subsec_requested = true,
Ok(false) => {}
Err(_) => return Value::build_text(""),
Err(_) => return None,
}

if matches!(parsed, Ok(Modifier::Floor) | Ok(Modifier::Ceiling)) {
n_floor = 0;
}
} else {
return Value::build_text("");
return None;
}
}

if is_leap_second(dt) || *dt > get_max_datetime_exclusive() {
return Value::build_text("");
if is_leap_second(&dt) || dt > get_max_datetime_exclusive() {
return None;
}

format_dt(*dt, output_type, subsec_requested)
Some(DtTransform {
dt,
subsec_requested,
})
}

fn format_dt(dt: NaiveDateTime, output_type: DateTimeOutput, subsec: bool) -> Value {
Expand All @@ -155,6 +184,16 @@ fn format_dt(dt: NaiveDateTime, output_type: DateTimeOutput, subsec: bool) -> Va
}
DateTimeOutput::StrfTime(format_str) => Value::from_text(strftime_format(&dt, &format_str)),
DateTimeOutput::JuliaDay => Value::Float(to_julian_day_exact(&dt)),
DateTimeOutput::UnixEpoch => {
if subsec {
let dt_utc = dt.and_utc();
let secs = dt_utc.timestamp();
let millis = dt_utc.timestamp_subsec_millis();
Value::Float(secs as f64 + (millis as f64 / 1000.0f64))
} else {
Value::Integer(dt.and_utc().timestamp())
}
}
}
}

Expand Down Expand Up @@ -420,21 +459,6 @@ fn to_julian_day_exact(dt: &NaiveDateTime) -> f64 {
i_jd as f64 / 86400000.0
}

pub fn exec_unixepoch(time_value: &Value) -> Value {
let dt = parse_naive_date_time(time_value);
match dt {
Some(dt) if !is_leap_second(&dt) => Value::Integer(get_unixepoch_from_naive_datetime(dt)),
_ => Value::Null,
}
}

fn get_unixepoch_from_naive_datetime(value: NaiveDateTime) -> i64 {
if is_leap_second(&value) {
return 0;
}
value.and_utc().timestamp()
}

fn parse_naive_date_time(time_value: impl AsValueRef) -> Option<NaiveDateTime> {
let time_value = time_value.as_value_ref();
match time_value {
Expand Down
16 changes: 6 additions & 10 deletions core/translate/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1641,20 +1641,16 @@ pub fn translate_expr(
Ok(target_register)
}
ScalarFunc::UnixEpoch => {
let mut start_reg = 0;
if args.len() > 1 {
crate::bail_parse_error!("epoch function with > 1 arguments. Modifiers are not yet supported.");
}
if args.len() == 1 {
let arg_reg = program.alloc_register();
let _ = translate_expr(
let start_reg = program.alloc_registers(args.len().max(1));
for (i, arg) in args.iter().enumerate() {
// register containing result of each argument expression
translate_expr(
program,
referenced_tables,
&args[0],
arg_reg,
arg,
start_reg + i,
resolver,
)?;
start_reg = arg_reg;
}
program.emit_insn(Insn::Function {
constant_mask: 0,
Expand Down
10 changes: 4 additions & 6 deletions core/vdbe/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5212,12 +5212,10 @@ pub fn op_function(
state.registers[*dest] = Register::Value(result);
}
ScalarFunc::UnixEpoch => {
let value = if *start_reg == 0 {
&Value::build_text("now")
} else {
state.registers[*start_reg].get_value()
};
state.registers[*dest] = Register::Value(exec_unixepoch(value));
let values =
registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]);
let result = exec_unixepoch(values);
state.registers[*dest] = Register::Value(result);
}
ScalarFunc::TursoVersion => {
if !program.connection.is_db_initialized() {
Expand Down
32 changes: 30 additions & 2 deletions testing/scalar-functions-datetime.test
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,34 @@ do_execsql_test date-with-subsec {
SELECT date('2023-05-18 15:30:45.123', 'subsec');
} {2023-05-18}

do_execsql_test unixepoch-with-modifier-add-days {
SELECT unixepoch('2025-12-23 21:03:55', '+1 days');
} {1766610235}

do_execsql_test unixepoch-with-modifier-substract-days {
SELECT unixepoch('2025-12-23 21:03:55', '-1 days');
} {1766437435}

do_execsql_test unixepoch-with-modifier-add-hours {
SELECT unixepoch('2025-12-23 21:03:55', '+6 hours');
} {1766545435}

do_execsql_test unixepoch-with-modifier-subtract-hours {
SELECT unixepoch('2025-12-23 21:03:55', '-6 hours');
} {1766502235}

do_execsql_test unixepoch-with-modifier-subtract-hours {
SELECT unixepoch('2025-12-23 21:03:55', 'start of month');
} {1764547200}

do_execsql_test unixepoch-with-modifier-subtract-hours {
SELECT unixepoch('2025-12-23 21:03:55', 'start of year');
} {1735689600}

do_execsql_test unixepoch-with-modifier-weekday {
SELECT unixepoch('2023-12-13 21:03:55', 'weekday 0');
} {1702847035}

do_execsql_test time-with-modifier-add-hours {
SELECT time('2023-05-18 15:30:45', '+5 hours');
} {20:30:45}
Expand Down Expand Up @@ -528,8 +556,8 @@ do_execsql_test strftime-day-without-leading-zero-1 {

do_execsql_test strftime-day-without-leading-zero-2 {
SELECT strftime('%e', '2025-01-02T13:10:30.567');
} {" 2"}
# TODO not a typo in sqlite there is also a space
} {" 2"}
# TODO not a typo in sqlite there is also a space

do_execsql_test strftime-fractional-seconds {
SELECT strftime('%f', '2025-01-02T13:10:30.567');
Expand Down
Loading