Merge pull request #252 from sonhmai/feat/158-partial-support-function-time

feat: add time() scalar function partial support without modifier #158
This commit is contained in:
Pekka Enberg
2024-08-01 09:22:08 +03:00
committed by GitHub
7 changed files with 408 additions and 19 deletions

View File

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

View File

@@ -87,15 +87,7 @@ fn get_max_datetime_exclusive() -> NaiveDateTime {
}
pub fn get_date_from_time_value(time_value: &OwnedValue) -> crate::Result<String> {
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<String
}
}
pub fn get_time_from_datetime_value(time_value: &OwnedValue) -> crate::Result<String> {
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<NaiveDateTime, DateTimeError> {
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<NaiveDateTime, DateTimeError> {
// 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
);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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