From 789ae4becfb8665cf38539c69bda828d0590dee7 Mon Sep 17 00:00:00 2001 From: sonhmai <> Date: Thu, 1 Aug 2024 09:55:38 +0700 Subject: [PATCH] feat: add time() scalar function partial support without modifier #158 --- COMPAT.md | 18 +-- core/datetime.rs | 275 ++++++++++++++++++++++++++++++++-- core/error.rs | 2 + core/function.rs | 3 + core/translate/expr.rs | 24 +++ core/vdbe/mod.rs | 25 +++- testing/scalar-functions.test | 80 ++++++++++ 7 files changed, 408 insertions(+), 19 deletions(-) diff --git a/COMPAT.md b/COMPAT.md index a4d4b68f8..fc26baa43 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -137,15 +137,15 @@ This document describes the SQLite compatibility status of Limbo: ### Date and time functions -| Function | Status | Comment | -|------------------------------|---------|---------| -| date() | Partial | | -| time() | No | | -| datetime() | No | | -| julianday() | No | | -| unixepoch() | No | | -| strftime() | No | | -| timediff() | No | | +| Function | Status | Comment | +|------------------------------|---------|------------------------| +| date() | Partial | | +| time() | Partial | not supported modifier | +| datetime() | No | | +| julianday() | No | | +| unixepoch() | No | | +| strftime() | No | | +| timediff() | No | | ### JSON functions diff --git a/core/datetime.rs b/core/datetime.rs index 7669f2efe..a3e7c7636 100644 --- a/core/datetime.rs +++ b/core/datetime.rs @@ -87,15 +87,7 @@ fn get_max_datetime_exclusive() -> NaiveDateTime { } pub fn get_date_from_time_value(time_value: &OwnedValue) -> crate::Result { - let dt = match time_value { - OwnedValue::Text(s) => get_date_time_from_time_value_string(s), - OwnedValue::Integer(i) => get_date_time_from_time_value_integer(*i), - OwnedValue::Float(f) => get_date_time_from_time_value_float(*f), - _ => Err(DateTimeError::InvalidArgument(format!( - "Invalid time value: {}", - time_value - ))), - }; + let dt = parse_naive_date_time(time_value); if dt.is_ok() { return Ok(get_date_from_naive_datetime(dt.unwrap())); } else { @@ -112,6 +104,36 @@ pub fn get_date_from_time_value(time_value: &OwnedValue) -> crate::Result crate::Result { + let dt = parse_naive_date_time(time_value); + if dt.is_ok() { + return Ok(get_time_from_naive_datetime(dt.unwrap())); + } else { + match dt.unwrap_err() { + DateTimeError::InvalidArgument(_) => { + trace!("Invalid time value: {}", time_value); + Ok(String::new()) + } + DateTimeError::Other(s) => { + trace!("Other date time error: {}", s); + Err(crate::error::LimboError::InvalidTime(s)) + } + } + } +} + +fn parse_naive_date_time(time_value: &OwnedValue) -> Result { + match time_value { + OwnedValue::Text(s) => get_date_time_from_time_value_string(s), + OwnedValue::Integer(i) => get_date_time_from_time_value_integer(*i), + OwnedValue::Float(f) => get_date_time_from_time_value_float(*f), + _ => Err(DateTimeError::InvalidArgument(format!( + "Invalid time value: {}", + time_value + ))), + } +} + fn get_date_time_from_time_value_string(value: &str) -> Result { // Time-value formats: // 1-7. YYYY-MM-DD[THH:MM[:SS[.SSS]]] @@ -233,6 +255,15 @@ fn get_date_from_naive_datetime(value: NaiveDateTime) -> String { 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() +} + mod tests { use super::*; use std::rc::Rc; @@ -501,4 +532,230 @@ mod tests { ); } } + + #[test] + fn test_valid_get_time_from_datetime_value() { + let now = chrono::Local::now().to_utc().format("%H:%M:%S").to_string(); + + let test_time_str = "22:30:45"; + let prev_time_str = "20:30:45"; + let next_time_str = "03:30:45"; + + let test_cases = [ + // Format 1: YYYY-MM-DD (no timezone applicable) + ( + OwnedValue::Text(Rc::new("2024-07-21".to_string())), + "00:00:00", + ), + // Format 2: YYYY-MM-DD HH:MM + ( + OwnedValue::Text(Rc::new("2024-07-21 22:30".to_string())), + "22:30:00", + ), + ( + OwnedValue::Text(Rc::new("2024-07-21 22:30+02:00".to_string())), + "20:30:00", + ), + ( + OwnedValue::Text(Rc::new("2024-07-21 22:30-05:00".to_string())), + "03:30:00", + ), + ( + OwnedValue::Text(Rc::new("2024-07-21 22:30Z".to_string())), + "22:30:00", + ), + // Format 3: YYYY-MM-DD HH:MM:SS + ( + OwnedValue::Text(Rc::new("2024-07-21 22:30:45".to_string())), + test_time_str, + ), + ( + OwnedValue::Text(Rc::new("2024-07-21 22:30:45+02:00".to_string())), + prev_time_str, + ), + ( + OwnedValue::Text(Rc::new("2024-07-21 22:30:45-05:00".to_string())), + next_time_str, + ), + ( + OwnedValue::Text(Rc::new("2024-07-21 22:30:45Z".to_string())), + test_time_str, + ), + // Format 4: YYYY-MM-DD HH:MM:SS.SSS + ( + OwnedValue::Text(Rc::new("2024-07-21 22:30:45.123".to_string())), + test_time_str, + ), + ( + OwnedValue::Text(Rc::new("2024-07-21 22:30:45.123+02:00".to_string())), + prev_time_str, + ), + ( + OwnedValue::Text(Rc::new("2024-07-21 22:30:45.123-05:00".to_string())), + next_time_str, + ), + ( + OwnedValue::Text(Rc::new("2024-07-21 22:30:45.123Z".to_string())), + test_time_str, + ), + // Format 5: YYYY-MM-DDTHH:MM + ( + OwnedValue::Text(Rc::new("2024-07-21T22:30".to_string())), + "22:30:00", + ), + ( + OwnedValue::Text(Rc::new("2024-07-21T22:30+02:00".to_string())), + "20:30:00", + ), + ( + OwnedValue::Text(Rc::new("2024-07-21T22:30-05:00".to_string())), + "03:30:00", + ), + ( + OwnedValue::Text(Rc::new("2024-07-21T22:30Z".to_string())), + "22:30:00", + ), + // Format 6: YYYY-MM-DDTHH:MM:SS + ( + OwnedValue::Text(Rc::new("2024-07-21T22:30:45".to_string())), + test_time_str, + ), + ( + OwnedValue::Text(Rc::new("2024-07-21T22:30:45+02:00".to_string())), + prev_time_str, + ), + ( + OwnedValue::Text(Rc::new("2024-07-21T22:30:45-05:00".to_string())), + next_time_str, + ), + ( + OwnedValue::Text(Rc::new("2024-07-21T22:30:45Z".to_string())), + test_time_str, + ), + // Format 7: YYYY-MM-DDTHH:MM:SS.SSS + ( + OwnedValue::Text(Rc::new("2024-07-21T22:30:45.123".to_string())), + test_time_str, + ), + ( + OwnedValue::Text(Rc::new("2024-07-21T22:30:45.123+02:00".to_string())), + prev_time_str, + ), + ( + OwnedValue::Text(Rc::new("2024-07-21T22:30:45.123-05:00".to_string())), + next_time_str, + ), + ( + OwnedValue::Text(Rc::new("2024-07-21T22:30:45.123Z".to_string())), + test_time_str, + ), + // Format 8: HH:MM + (OwnedValue::Text(Rc::new("22:30".to_string())), "22:30:00"), + ( + OwnedValue::Text(Rc::new("22:30+02:00".to_string())), + "20:30:00", + ), + ( + OwnedValue::Text(Rc::new("22:30-05:00".to_string())), + "03:30:00", + ), + (OwnedValue::Text(Rc::new("22:30Z".to_string())), "22:30:00"), + // Format 9: HH:MM:SS + ( + OwnedValue::Text(Rc::new("22:30:45".to_string())), + test_time_str, + ), + ( + OwnedValue::Text(Rc::new("22:30:45+02:00".to_string())), + prev_time_str, + ), + ( + OwnedValue::Text(Rc::new("22:30:45-05:00".to_string())), + next_time_str, + ), + ( + OwnedValue::Text(Rc::new("22:30:45Z".to_string())), + test_time_str, + ), + // Format 10: HH:MM:SS.SSS + ( + OwnedValue::Text(Rc::new("22:30:45.123".to_string())), + test_time_str, + ), + ( + OwnedValue::Text(Rc::new("22:30:45.123+02:00".to_string())), + prev_time_str, + ), + ( + OwnedValue::Text(Rc::new("22:30:45.123-05:00".to_string())), + next_time_str, + ), + ( + OwnedValue::Text(Rc::new("22:30:45.123Z".to_string())), + test_time_str, + ), + // Test Format 11: 'now' + (OwnedValue::Text(Rc::new("now".to_string())), &now), + // Format 12: DDDDDDDDDD (Julian date as float or integer) + (OwnedValue::Float(2460082.1), "14:24:00"), + (OwnedValue::Integer(2460082), "12:00:00"), + ]; + + for (input, expected) in test_cases { + assert_eq!( + get_time_from_datetime_value(&input).unwrap(), + expected, + "Failed for input: {:?}", + input + ); + } + } + + #[test] + fn test_invalid_get_time_from_datetime_value() { + let invalid_cases = [ + OwnedValue::Text(Rc::new("2024-07-21 25:00".to_string())), // Invalid hour + OwnedValue::Text(Rc::new("2024-07-21 24:00:00".to_string())), // Invalid hour + OwnedValue::Text(Rc::new("2024-07-21 23:60:00".to_string())), // Invalid minute + OwnedValue::Text(Rc::new("2024-07-21 22:58:60".to_string())), // Invalid second + OwnedValue::Text(Rc::new("2024-07-32".to_string())), // Invalid day + OwnedValue::Text(Rc::new("2024-13-01".to_string())), // Invalid month + OwnedValue::Text(Rc::new("invalid_date".to_string())), // Completely invalid string + OwnedValue::Text(Rc::new("".to_string())), // Empty string + OwnedValue::Integer(i64::MAX), // Large Julian day + OwnedValue::Integer(-1), // Negative Julian day + OwnedValue::Float(f64::MAX), // Large float + OwnedValue::Float(-1.0), // Negative Julian day as float + OwnedValue::Float(f64::NAN), // NaN + OwnedValue::Float(f64::INFINITY), // Infinity + OwnedValue::Null, // Null value + OwnedValue::Blob(vec![1, 2, 3].into()), // Blob (unsupported type) + // Invalid timezone tests + OwnedValue::Text(Rc::new("2024-07-21T12:00:00+24:00".to_string())), // Invalid timezone offset (too large) + OwnedValue::Text(Rc::new("2024-07-21T12:00:00-24:00".to_string())), // Invalid timezone offset (too small) + OwnedValue::Text(Rc::new("2024-07-21T12:00:00+00:60".to_string())), // Invalid timezone minutes + OwnedValue::Text(Rc::new("2024-07-21T12:00:00+00:00:00".to_string())), // Invalid timezone format (extra seconds) + OwnedValue::Text(Rc::new("2024-07-21T12:00:00+".to_string())), // Incomplete timezone + OwnedValue::Text(Rc::new("2024-07-21T12:00:00+Z".to_string())), // Invalid timezone format + OwnedValue::Text(Rc::new("2024-07-21T12:00:00+00:00Z".to_string())), // Mixing offset and Z + OwnedValue::Text(Rc::new("2024-07-21T12:00:00UTC".to_string())), // Named timezone (not supported) + ]; + + for case in invalid_cases.iter() { + let result = get_time_from_datetime_value(case); + assert!( + result.is_ok(), + "Error encountered while parsing time value {}: {}", + case, + result.unwrap_err() + ); + let result_str = result.unwrap(); + assert!( + result_str.is_empty(), + "Expected empty string for input: {:?}, but got: {:?}", + case, + result_str + ); + } + } } diff --git a/core/error.rs b/core/error.rs index 765d6f4e6..cf59ff5f9 100644 --- a/core/error.rs +++ b/core/error.rs @@ -32,6 +32,8 @@ pub enum LimboError { ParseFloatError(#[from] std::num::ParseFloatError), #[error("Parse error: {0}")] InvalidDate(String), + #[error("Parse error: {0}")] + InvalidTime(String), } #[macro_export] diff --git a/core/function.rs b/core/function.rs index 75b53f940..ded9907bf 100644 --- a/core/function.rs +++ b/core/function.rs @@ -41,6 +41,7 @@ pub enum ScalarFunc { Min, Max, Date, + Time, Unicode, } @@ -61,6 +62,7 @@ impl ToString for ScalarFunc { ScalarFunc::Min => "min".to_string(), ScalarFunc::Max => "max".to_string(), ScalarFunc::Date => "date".to_string(), + ScalarFunc::Time => "time".to_string(), ScalarFunc::Unicode => "unicode".to_string(), } } @@ -97,6 +99,7 @@ impl Func { "round" => Ok(Func::Scalar(ScalarFunc::Round)), "length" => Ok(Func::Scalar(ScalarFunc::Length)), "date" => Ok(Func::Scalar(ScalarFunc::Date)), + "time" => Ok(Func::Scalar(ScalarFunc::Time)), "unicode" => Ok(Func::Scalar(ScalarFunc::Unicode)), _ => Err(()), } diff --git a/core/translate/expr.rs b/core/translate/expr.rs index deb81ecc0..49dec6b55 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -275,6 +275,30 @@ pub fn translate_expr( }); Ok(target_register) } + ScalarFunc::Time => { + let mut start_reg = 0; + if let Some(args) = args { + if args.len() > 1 { + crate::bail_parse_error!("date function with > 1 arguments. Modifiers are not yet supported."); + } else if args.len() == 1 { + let arg_reg = program.alloc_register(); + let _ = translate_expr( + program, + select, + &args[0], + arg_reg, + cursor_hint, + )?; + start_reg = arg_reg; + } + } + program.emit_insn(Insn::Function { + start_reg: start_reg, + dest: target_register, + func: ScalarFunc::Time, + }); + Ok(target_register) + } ScalarFunc::Trim | ScalarFunc::LTrim | ScalarFunc::RTrim diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 5e68f93b3..25f166e40 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -22,7 +22,7 @@ pub mod explain; pub mod sorter; use crate::btree::BTreeCursor; -use crate::datetime::get_date_from_time_value; +use crate::datetime::{get_date_from_time_value, get_time_from_datetime_value}; use crate::error::LimboError; use crate::function::{AggFunc, ScalarFunc}; use crate::pager::Pager; @@ -1258,6 +1258,29 @@ impl Program { } state.pc += 1; } + ScalarFunc::Time => { + if *start_reg == 0 { + let time_str = get_time_from_datetime_value(&OwnedValue::Text( + Rc::new("now".to_string()), + ))?; + state.registers[*dest] = OwnedValue::Text(Rc::new(time_str)); + } else { + let datetime_value = &state.registers[*start_reg]; + let time_str = get_time_from_datetime_value(datetime_value); + match time_str { + Ok(time) => { + state.registers[*dest] = OwnedValue::Text(Rc::new(time)) + } + Err(e) => { + return Err(LimboError::ParseError(format!( + "Error encountered while parsing time value: {}", + e + ))); + } + } + } + state.pc += 1; + } ScalarFunc::Unicode => { let reg_value = state.registers[*start_reg].borrow_mut(); state.registers[*dest] = exec_unicode(reg_value); diff --git a/testing/scalar-functions.test b/testing/scalar-functions.test index eb10ba8b1..c83a753e7 100755 --- a/testing/scalar-functions.test +++ b/testing/scalar-functions.test @@ -315,6 +315,86 @@ do_execsql_test date-with-invalid-timezone { SELECT date('2023-05-18 15:30:45+25:00'); } {{}} +do_execsql_test time-no-arg { + SELECT length(time()) = 8; +} {1} + +do_execsql_test time-current-time { + SELECT length(time('now')) = 8; +} {1} + +do_execsql_test time-specific-time { + SELECT time('04:02:00'); +} {04:02:00} + +do_execsql_test time-of-datetime { + SELECT time('2023-05-18 15:30:45'); +} {15:30:45} + +do_execsql_test time-iso8601 { + SELECT time('2023-05-18T15:30:45'); +} {15:30:45} + +do_execsql_test time-with-milliseconds { + SELECT time('2023-05-18 15:30:45.123'); +} {15:30:45} + +do_execsql_test time-julian-day-integer { + SELECT time(2460082); +} {12:00:00} + +do_execsql_test time-julian-day-float { + SELECT time(2460082.2); +} {16:48:00} + +do_execsql_test time-invalid-input { + SELECT time('not a time'); +} {{}} + +do_execsql_test time-null-input { + SELECT time(NULL); +} {{}} + +do_execsql_test time-out-of-range { + SELECT time('25:05:01'); +} {{}} + +do_execsql_test time-date-only { + SELECT time('2024-02-02'); +} {00:00:00} + +do_execsql_test time-with-timezone-utc { + SELECT time('2023-05-18 15:30:45Z'); +} {15:30:45} + +do_execsql_test time-with-timezone-positive { + SELECT time('2023-05-18 23:30:45+07:00'); +} {16:30:45} + +do_execsql_test time-with-timezone-negative { + SELECT time('2023-05-19 01:30:45-05:00'); +} {06:30:45} + +do_execsql_test time-with-timezone-day-change-positive { + SELECT time('2023-05-18 23:30:45-03:00'); +} {02:30:45} + +do_execsql_test time-with-timezone-day-change-negative { + SELECT time('2023-05-19 01:30:45+03:00'); +} {22:30:45} + +do_execsql_test time-with-timezone-iso8601 { + SELECT time('2023-05-18T15:30:45+02:00'); +} {13:30:45} + +do_execsql_test time-with-timezone-and-milliseconds { + SELECT time('2023-05-18 15:30:45.123+02:00'); +} {13:30:45} + +do_execsql_test time-with-invalid-timezone { + SELECT time('2023-05-18 15:30:45+25:00'); +} {{}} + do_execsql_test unicode-a { SELECT unicode('a'); } {97}