mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-04 17:04:18 +01:00
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:
18
COMPAT.md
18
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
|
||||
|
||||
|
||||
275
core/datetime.rs
275
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<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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(()),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user