mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-03 07:14:33 +01:00
checkpoint: implemented time_now, time_fmt_iso, time_date
This commit is contained in:
31
Cargo.lock
generated
31
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -18,7 +18,8 @@ members = [
|
||||
"sqlite3",
|
||||
"tests",
|
||||
"extensions/percentile",
|
||||
"extensions/vector",
|
||||
"extensions/vector",
|
||||
"extensions/time",
|
||||
]
|
||||
exclude = ["perf/latency/limbo"]
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
20
extensions/time/Cargo.toml
Normal file
20
extensions/time/Cargo.toml
Normal 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
127
extensions/time/src/lib.rs
Normal 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
208
extensions/time/src/time.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user