checkpoint: implemented time_now, time_fmt_iso, time_date

This commit is contained in:
pedrocarlo
2025-01-30 01:26:47 -03:00
parent 489e3242c9
commit 643ad147c0
7 changed files with 395 additions and 2 deletions

31
Cargo.lock generated
View File

@@ -1569,6 +1569,7 @@ dependencies = [
"limbo_macros",
"limbo_percentile",
"limbo_regexp",
"limbo_time",
"limbo_uuid",
"limbo_vector",
"log",
@@ -1661,6 +1662,17 @@ dependencies = [
"log",
]
[[package]]
name = "limbo_time"
version = "0.0.13"
dependencies = [
"chrono",
"limbo_ext",
"strum",
"strum_macros",
"thiserror 2.0.11",
]
[[package]]
name = "limbo_uuid"
version = "0.0.13"
@@ -2729,6 +2741,25 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.96",
]
[[package]]
name = "supports-color"
version = "3.0.2"

View File

@@ -18,7 +18,8 @@ members = [
"sqlite3",
"tests",
"extensions/percentile",
"extensions/vector",
"extensions/vector",
"extensions/time",
]
exclude = ["perf/latency/limbo"]

View File

@@ -14,7 +14,7 @@ name = "limbo_core"
path = "lib.rs"
[features]
default = ["fs", "json", "uuid", "vector", "io_uring"]
default = ["fs", "json", "uuid", "vector", "io_uring", "time"]
fs = []
json = [
"dep:jsonb",
@@ -26,6 +26,7 @@ vector = ["limbo_vector/static"]
io_uring = ["dep:io-uring", "rustix/io_uring"]
percentile = ["limbo_percentile/static"]
regexp = ["limbo_regexp/static"]
time = ["limbo_time/static"]
[target.'cfg(target_os = "linux")'.dependencies]
io-uring = { version = "0.6.1", optional = true }
@@ -65,6 +66,7 @@ limbo_uuid = { path = "../extensions/uuid", optional = true, features = ["static
limbo_vector = { path = "../extensions/vector", optional = true, features = ["static"] }
limbo_regexp = { path = "../extensions/regexp", optional = true, features = ["static"] }
limbo_percentile = { path = "../extensions/percentile", optional = true, features = ["static"] }
limbo_time = { path = "../extensions/time", optional = true, features = ["static"] }
miette = "7.4.0"
[build-dependencies]

View File

@@ -92,6 +92,10 @@ impl Database {
if unsafe { !limbo_regexp::register_extension_static(&ext_api).is_ok() } {
return Err("Failed to register regexp extension".to_string());
}
#[cfg(feature = "time")]
if unsafe { !limbo_time::register_extension_static(&ext_api).is_ok() } {
return Err("Failed to register time extension".to_string());
}
Ok(())
}
}

View File

@@ -0,0 +1,20 @@
[package]
authors.workspace = true
edition.workspace = true
license.workspace = true
name = "limbo_time"
repository.workspace = true
version.workspace = true
[lib]
crate-type = ["cdylib", "lib"]
[features]
static = ["limbo_ext/static"]
[dependencies]
chrono = "0.4.39"
limbo_ext = { path = "../core", features = ["static"] }
strum = "0.26.3"
strum_macros = "0.26.3"
thiserror = "2.0.11"

127
extensions/time/src/lib.rs Normal file
View File

@@ -0,0 +1,127 @@
use chrono::prelude::*;
use chrono::NaiveDateTime;
use thiserror::Error;
use limbo_ext::{register_extension, scalar, ResultCode, Value};
mod time;
use time::*;
register_extension! {
scalars: {time_now, time_fmt_iso, time_date},
}
macro_rules! ok_tri {
($e:expr $(,)?) => {
match $e {
Some(val) => val,
None => return Value::error(ResultCode::Error),
}
};
}
macro_rules! tri {
($e:expr $(,)?) => {
match $e {
Ok(val) => val,
Err(_) => return Value::error(ResultCode::Error),
}
};
}
#[derive(Error, Debug)]
pub enum TimeError {
/// Timezone offset is invalid
#[error("invalid timezone offset")]
InvalidOffset,
#[error("invalid datetime format")]
InvalidFormat,
/// Blob is not size of `TIME_BLOB_SIZE`
#[error("blob is not correct size")]
InvalidSize,
/// Blob time version not matching
#[error("time blob version mismatch")]
MismatchVersion,
#[error("time blob version mismatch")]
UnknownField(#[from] <TimeField as ::core::str::FromStr>::Err),
}
type Result<T> = core::result::Result<T, TimeError>;
#[scalar(name = "time_now", alias = "now")]
fn time_now(args: &[Value]) -> Value {
if args.len() != 0 {
return Value::error(ResultCode::Error);
}
let t = Time::new();
t.into_blob()
}
#[scalar(name = "time_fmt_iso")]
fn time_fmt_iso(args: &[Value]) -> Value {
if args.len() != 1 && args.len() != 2 {
return Value::error(ResultCode::Error);
}
let blob = ok_tri!(args[0].to_blob());
let t = tri!(Time::try_from(blob));
let offset_sec = {
if args.len() == 2 {
ok_tri!(args[1].to_integer()) as i32
} else {
0
}
};
let fmt_str = tri!(t.fmt_iso(offset_sec));
Value::from_text(fmt_str)
}
/// \
/// Caveat: this function differs from sqlean's as it does not support normalizing the inputs
/// For example, October 32 will error. It will not normalize to November 1.\
/// Also due to a current limitation in the extension system, the function can only have one alias
/// and the alias cannot have different number of arguments. So no aliasing for now for this one.
#[scalar(name = "time_date")]
fn time_date(args: &[Value]) -> Value {
if args.len() != 3 && args.len() != 6 && args.len() != 7 && args.len() != 8 {
return Value::error(ResultCode::Error);
}
let year = ok_tri!(&args[0].to_integer()).to_owned() as i32;
let month = ok_tri!(&args[1].to_integer()).to_owned() as u32;
let day = ok_tri!(&args[2].to_integer()).to_owned() as u32;
let mut datetime: NaiveDateTime = ok_tri!(NaiveDate::from_ymd_opt(year, month, day))
.and_hms_opt(0, 0, 0)
.unwrap();
if args.len() >= 6 {
let hour = ok_tri!(&args[3].to_integer()).to_owned() as u32;
let minute = ok_tri!(&args[4].to_integer()).to_owned() as u32;
let seconds = ok_tri!(&args[5].to_integer()).to_owned() as u32;
datetime = ok_tri!(datetime.with_hour(hour));
datetime = ok_tri!(datetime.with_minute(minute));
datetime = ok_tri!(datetime.with_second(seconds));
}
if args.len() >= 7 {
let nano_sec = ok_tri!(&args[6].to_integer()).to_owned() as u32;
datetime = ok_tri!(datetime.with_nanosecond(nano_sec));
}
if args.len() == 8 {
let offset_sec = ok_tri!(&args[7].to_integer()).to_owned() as i32;
let offset = ok_tri!(FixedOffset::east_opt(offset_sec));
// I believe this is not a double conversion here
datetime = ok_tri!(datetime.and_local_timezone(offset).single()).naive_utc();
}
let t = Time::from_datetime(datetime.and_utc());
t.into_blob()
}

208
extensions/time/src/time.rs Normal file
View File

@@ -0,0 +1,208 @@
use std::str::FromStr;
use chrono::prelude::*;
use chrono::{self, DateTime, NaiveDate, Timelike, Utc};
use limbo_ext::Value;
use crate::{Result, TimeError};
const DAYS_BEFORE_EPOCH: i64 = 719162;
const TIME_BLOB_SIZE: usize = 13;
const VERSION: u8 = 1;
#[derive(Debug)]
enum TimePrecision {
Seconds,
Millis,
Micro,
Nano,
}
#[derive(Debug)]
pub struct Time {
// seconds: i64,
// nanoseconds: u32,
inner: DateTime<Utc>,
}
#[derive(Debug)]
pub struct Duration {
inner: i64,
}
#[derive(strum_macros::Display, strum_macros::EnumString)]
pub enum TimeField {
#[strum(to_string = "millennium")]
Millennium,
#[strum(to_string = "century")]
Century,
#[strum(to_string = "decade")]
Decade,
#[strum(to_string = "year")]
Year,
#[strum(to_string = "quarter")]
Quarter,
#[strum(to_string = "month")]
Month,
#[strum(to_string = "day")]
Day,
#[strum(to_string = "hour")]
Hour,
#[strum(to_string = "minute")]
Minute,
#[strum(to_string = "second")]
Second,
#[strum(to_string = "millisecond")]
MilliSecond,
#[strum(to_string = "milli")]
Milli,
#[strum(to_string = "microsecond")]
MicroSecond,
#[strum(to_string = "micro")]
Micro,
#[strum(to_string = "nanosecond")]
NanoSecond,
#[strum(to_string = "nano")]
Nano,
#[strum(to_string = "isoyear")]
IsoYear,
#[strum(to_string = "isoweek")]
IsoWeek,
#[strum(to_string = "isodow")]
IsoDow,
#[strum(to_string = "yearday")]
YearDay,
#[strum(to_string = "weekday")]
WeekDay,
#[strum(to_string = "epoch")]
Epoch,
}
impl Time {
pub fn new() -> Self {
Self { inner: Utc::now() }
}
pub fn into_blob(self) -> Value {
let blob: [u8; 13] = self.into();
Value::from_blob(blob.to_vec())
}
pub fn fmt_iso(&self, offset_sec: i32) -> Result<String> {
if offset_sec == 0 {
if self.inner.nanosecond() == 0 {
return Ok(self.inner.format("%FT%TZ").to_string());
} else {
return Ok(self.inner.format("%FT%T%.9fZ").to_string());
}
}
// I do not see how this can error
let offset = &FixedOffset::east_opt(offset_sec).ok_or(TimeError::InvalidFormat)?;
let timezone_date = self.inner.with_timezone(offset);
if timezone_date.nanosecond() == 0 {
return Ok(timezone_date.format("%FT%T%:z").to_string());
} else {
return Ok(timezone_date.format("%FT%T%.9f%:z").to_string());
}
}
/// Adjust the datetime to the offset
pub fn from_datetime(dt: DateTime<Utc>) -> Self {
Self { inner: dt }
}
pub fn time_get(&self, field: &str) -> Result<Value> {
use TimeField::*;
chrono::format::strftime
self.inner.format_with_items(items)
let val = match TimeField::from_str(field)? {
Millennium => Value::from_integer((self.inner.year() / 1000) as i64),
Century => Value::from_integer((self.inner.year() / 100) as i64),
Decade => Value::from_integer((self.inner.year() / 10) as i64),
Year => Value::from_integer(self.inner.year() as i64),
Quarter => Value::from_integer((self.inner.month().div_ceil(4) + 1) as i64),
Month => Value::from_integer(self.inner.month() as i64),
Day => Value::from_integer(self.inner.day() as i64),
Hour => Value::from_integer(self.inner.hour() as i64),
Minute => Value::from_integer(self.inner.minute() as i64),
Second => Value::from_float(f64::from_str(s))
MilliSecond | Milli => Value:: from_integer(self.inner.)
};
Ok(val)
}
}
impl From<Time> for [u8; TIME_BLOB_SIZE] {
fn from(value: Time) -> Self {
let mut blob = [0u8; 13];
let seconds = value.inner.timestamp() + (3600 * 24 * DAYS_BEFORE_EPOCH);
let nanoseconds = value.inner.timestamp_subsec_nanos();
blob[0] = VERSION;
blob[1] = (seconds >> 56) as u8; // bytes 1-8: seconds
blob[2] = (seconds >> 48) as u8;
blob[3] = (seconds >> 40) as u8;
blob[4] = (seconds >> 32) as u8;
blob[5] = (seconds >> 24) as u8;
blob[6] = (seconds >> 16) as u8;
blob[7] = (seconds >> 8) as u8;
blob[8] = seconds as u8;
blob[9] = (nanoseconds >> 24) as u8; // bytes 9-12: nanoseconds
blob[10] = (nanoseconds >> 16) as u8;
blob[11] = (nanoseconds >> 8) as u8;
blob[12] = (nanoseconds) as u8;
blob
}
}
impl TryFrom<Vec<u8>> for Time {
type Error = TimeError;
fn try_from(value: Vec<u8>) -> Result<Time> {
if value.len() != TIME_BLOB_SIZE {
return Err(TimeError::InvalidSize);
}
if value[0] != VERSION {
return Err(TimeError::MismatchVersion);
}
let seconds = value[8] as i64
| (value[7] as i64) << 8
| (value[6] as i64) << 16
| (value[5] as i64) << 24
| (value[4] as i64) << 32
| (value[3] as i64) << 40
| (value[2] as i64) << 48
| (value[1] as i64) << 56;
let nanoseconds = value[12] as u32
| (value[11] as u32) << 8
| (value[10] as u32) << 16
| (value[9] as u32) << 24;
Ok(Self {
inner: DateTime::from_timestamp(seconds - (3600 * 24 * DAYS_BEFORE_EPOCH), nanoseconds)
.ok_or_else(|| TimeError::InvalidFormat)?
.to_utc(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let t = Time::new();
}
}