Merge 'Sqlean Time extension' from Pedro Muniz

This PR implements a sqlean time compatible extension. I would
appreciate some help to review my code and see if there are ways to
enhance it. Also, if there is some edge case, I have missed please tell
me.
https://github.com/nalgeon/sqlean/blob/main/docs/time.md

Closes #854
This commit is contained in:
Pekka Enberg
2025-02-04 18:27:44 +02:00
10 changed files with 2083 additions and 2 deletions

View File

@@ -24,6 +24,7 @@ This document describes the compatibility of Limbo with SQLite.
- [UUID](#uuid)
- [regexp](#regexp)
- [Vector](#vector)
- [Time](#time)
## Features
@@ -630,3 +631,56 @@ The `vector` extension is compatible with libSQL native vector search.
| vector64(x) | Yes | |
| vector_extract(x) | Yes | |
| vector_distance_cos(x, y) | Yes | |
### Time
The `time` extension is compatible with [sqlean-time](https://github.com/nalgeon/sqlean/blob/main/docs/time.md).
| Function | Status | Comment |
| ------------------------------------------------------------------- | ------ | ---------------------------- |
| time_now() | Yes | |
| time_date(year, month, day[, hour, min, sec[, nsec[, offset_sec]]]) | Yes | offset_sec is not normalized |
| time_get_year(t) | Yes | |
| time_get_month(t) | Yes | |
| time_get_day(t) | Yes | |
| time_get_hour(t) | Yes | |
| time_get_minute(t) | Yes | |
| time_get_second(t) | Yes | |
| time_get_nano(t) | Yes | |
| time_get_weekday(t) | Yes | |
| time_get_yearday(t) | Yes | |
| time_get_isoyear(t) | Yes | |
| time_get_isoweek(t) | Yes | |
| time_get(t, field) | Yes | |
| time_unix(sec[, nsec]) | Yes | |
| time_milli(msec) | Yes | |
| time_micro(usec) | Yes | |
| time_nano(nsec) | Yes | |
| time_to_unix(t) | Yes | |
| time_to_milli(t) | Yes | |
| time_to_micro(t) | Yes | |
| time_to_nano(t) | Yes | |
| time_after(t, u) | Yes | |
| time_before(t, u) | Yes | |
| time_compare(t, u) | Yes | |
| time_equal(t, u) | Yes | |
| time_add(t, d) | Yes | |
| time_add_date(t, years[, months[, days]]) | Yes | |
| time_sub(t, u) | Yes | |
| time_since(t) | Yes | |
| time_until(t) | Yes | |
| time_trunc(t, field) | Yes | |
| time_trunc(t, d) | Yes | |
| time_round(t, d) | Yes | |
| time_fmt_iso(t[, offset_sec]) | Yes | |
| time_fmt_datetime(t[, offset_sec]) | Yes | |
| time_fmt_date(t[, offset_sec]) | Yes | |
| time_fmt_time(t[, offset_sec]) | Yes | |
| time_parse(s) | Yes | |
| dur_ns() | Yes | |
| dur_us() | Yes | |
| dur_ms() | Yes | |
| dur_s() | Yes | |
| dur_m() | Yes | |
| dur_h() | Yes | |

13
Cargo.lock generated
View File

@@ -1583,6 +1583,7 @@ dependencies = [
"limbo_macros",
"limbo_percentile",
"limbo_regexp",
"limbo_time",
"limbo_uuid",
"limbo_vector",
"log",
@@ -1678,6 +1679,18 @@ dependencies = [
"log",
]
[[package]]
name = "limbo_time"
version = "0.0.13"
dependencies = [
"chrono",
"limbo_ext",
"mimalloc",
"strum",
"strum_macros",
"thiserror 2.0.11",
]
[[package]]
name = "limbo_uuid"
version = "0.0.14"

View File

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

View File

@@ -82,6 +82,10 @@ test-vector:
SQLITE_EXEC=$(SQLITE_EXEC) ./testing/vector.test
.PHONY: test-vector
test-time:
SQLITE_EXEC=$(SQLITE_EXEC) ./testing/time.test
.PHONY: test-time
test-sqlite3: limbo-c
LIBS="$(SQLITE_LIB)" HEADERS="$(SQLITE_LIB_HEADERS)" make -C sqlite3/tests test
.PHONY: test-sqlite3

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"
strum = "0.26"

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,23 @@
[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"]
[target.'cfg(not(target_family = "wasm"))'.dependencies]
mimalloc = { version = "*", default-features = false }
[dependencies]
chrono = "0.4.39"
limbo_ext = { path = "../core", features = ["static"] }
strum = "0.26.3"
strum_macros = "0.26.3"
thiserror = "2.0.11"

1019
extensions/time/src/lib.rs Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,560 @@
use std::ops::{Deref, Sub};
use chrono::{self, DateTime, Timelike, Utc};
use chrono::{prelude::*, DurationRound};
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, PartialEq, PartialOrd, Eq)]
pub struct Time {
inner: DateTime<Utc>,
}
#[derive(Debug, PartialEq, Eq, PartialOrd)]
pub struct Duration {
inner: chrono::Duration,
}
#[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,
}
#[derive(strum_macros::Display, strum_macros::EnumString)]
pub enum TimeRoundField {
#[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 = "week")]
Week,
#[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,
}
impl Time {
/// Returns a new instance of Time with tracking UTC::now
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 {
Ok(timezone_date.format("%FT%T%:z").to_string())
} else {
Ok(timezone_date.format("%FT%T%.9f%:z").to_string())
}
}
pub fn fmt_datetime(&self, offset_sec: i32) -> Result<String> {
let fmt = "%F %T";
if offset_sec == 0 {
return Ok(self.inner.format(fmt).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);
Ok(timezone_date.format(fmt).to_string())
}
pub fn fmt_date(&self, offset_sec: i32) -> Result<String> {
let fmt = "%F";
if offset_sec == 0 {
return Ok(self.inner.format(fmt).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);
Ok(timezone_date.format(fmt).to_string())
}
pub fn fmt_time(&self, offset_sec: i32) -> Result<String> {
let fmt = "%T";
if offset_sec == 0 {
return Ok(self.inner.format(fmt).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);
Ok(timezone_date.format(fmt).to_string())
}
/// Adjust the datetime to the offset
pub fn from_datetime(dt: DateTime<Utc>) -> Self {
Self { inner: dt }
}
//
#[allow(clippy::too_many_arguments)]
pub fn time_date(
year: i32,
month: i32,
day: i64,
hour: i64,
minutes: i64,
seconds: i64,
nano_secs: i64,
offset: FixedOffset,
) -> Result<Self> {
let mut dt: NaiveDateTime = NaiveDate::from_ymd_opt(1, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap();
match year.cmp(&0) {
std::cmp::Ordering::Greater => {
dt = dt
.checked_add_months(chrono::Months::new((year - 1).unsigned_abs() * 12))
.ok_or(TimeError::CreationError)?
}
std::cmp::Ordering::Less => {
dt = dt
.checked_sub_months(chrono::Months::new((year - 1).unsigned_abs() * 12))
.ok_or(TimeError::CreationError)?
}
std::cmp::Ordering::Equal => (),
};
match month.cmp(&0) {
std::cmp::Ordering::Greater => {
dt = dt
.checked_add_months(chrono::Months::new((month - 1).unsigned_abs()))
.ok_or(TimeError::CreationError)?
}
std::cmp::Ordering::Less => {
dt = dt
.checked_sub_months(chrono::Months::new((month - 1).unsigned_abs()))
.ok_or(TimeError::CreationError)?
}
std::cmp::Ordering::Equal => (),
};
dt += chrono::Duration::try_days(day - 1).ok_or(TimeError::CreationError)?;
dt += chrono::Duration::try_hours(hour).ok_or(TimeError::CreationError)?;
dt += chrono::Duration::try_minutes(minutes).ok_or(TimeError::CreationError)?;
dt += chrono::Duration::try_seconds(seconds).ok_or(TimeError::CreationError)?;
dt += chrono::Duration::nanoseconds(nano_secs);
dt = dt
.and_local_timezone(offset)
.single()
.ok_or(TimeError::CreationError)?
.naive_utc();
Ok(dt.into())
}
pub fn time_add_date(self, years: i32, months: i32, days: i64) -> Result<Self> {
let mut dt: NaiveDateTime = self.into();
match years.cmp(&0) {
std::cmp::Ordering::Greater => {
dt = dt
.checked_add_months(chrono::Months::new(years.unsigned_abs() * 12))
.ok_or(TimeError::CreationError)?;
}
std::cmp::Ordering::Less => {
dt = dt
.checked_sub_months(chrono::Months::new(years.unsigned_abs() * 12))
.ok_or(TimeError::CreationError)?;
}
std::cmp::Ordering::Equal => (),
};
match months.cmp(&0) {
std::cmp::Ordering::Greater => {
dt = dt
.checked_add_months(chrono::Months::new(months.unsigned_abs()))
.ok_or(TimeError::CreationError)?
}
std::cmp::Ordering::Less => {
dt = dt
.checked_sub_months(chrono::Months::new(months.unsigned_abs()))
.ok_or(TimeError::CreationError)?
}
std::cmp::Ordering::Equal => (),
};
dt += chrono::Duration::try_days(days).ok_or(TimeError::CreationError)?;
Ok(dt.into())
}
pub fn get_second(&self) -> i64 {
self.inner.second() as i64
}
pub fn get_nanosecond(&self) -> i64 {
self.inner.timestamp_subsec_nanos() as i64
}
pub fn to_unix(&self) -> i64 {
self.inner.timestamp()
}
pub fn to_unix_milli(&self) -> i64 {
self.inner.timestamp_millis()
}
pub fn to_unix_micro(&self) -> i64 {
self.inner.timestamp_micros()
}
pub fn to_unix_nano(&self) -> Option<i64> {
self.inner.timestamp_nanos_opt()
}
pub fn add_duration(&self, d: Duration) -> Self {
Self {
inner: self.inner + d.inner,
}
}
pub fn sub_duration(&self, d: Duration) -> Self {
Self {
inner: self.inner - d.inner,
}
}
pub fn trunc_duration(&self, d: Duration) -> Result<Self> {
Ok(Self {
inner: self.inner.duration_trunc(d.inner)?,
})
}
pub fn trunc_field(&self, field: TimeRoundField) -> Result<Self> {
use TimeRoundField::*;
let year: i32;
let mut month: i32 = 1;
let mut week: i32 = 0;
let mut day: i64 = 1;
let mut hour: i64 = 0;
let mut minutes: i64 = 0;
let mut seconds: i64 = 0;
let mut nano_secs: i64 = 0;
let offset = FixedOffset::east_opt(0).unwrap(); // UTC
match field {
Millennium => {
let millennium = (self.inner.year() / 1000) * 1000;
year = millennium;
}
Century => {
let century = (self.inner.year() / 100) * 100;
year = century;
}
Decade => {
let decade = (self.inner.year() / 10) * 10;
year = decade;
}
Year => {
year = self.inner.year();
}
Quarter => {
let quarter = ((self.inner.month() - 1) / 3) as i32;
year = self.inner.year();
month = (quarter * 3) + 1;
}
Month => {
year = self.inner.year();
month = self.inner.month() as i32;
}
Week => {
let isoweek = self.inner.iso_week();
year = isoweek.year();
week = isoweek.week() as i32;
}
Day => {
year = self.inner.year();
month = self.inner.month() as i32;
day = self.inner.day() as i64;
}
Hour => {
year = self.inner.year();
month = self.inner.month() as i32;
day = self.inner.day() as i64;
hour = self.inner.hour() as i64;
}
Minute => {
year = self.inner.year();
month = self.inner.month() as i32;
day = self.inner.day() as i64;
hour = self.inner.hour() as i64;
minutes = self.inner.minute() as i64;
}
Second => {
year = self.inner.year();
month = self.inner.month() as i32;
day = self.inner.day() as i64;
hour = self.inner.hour() as i64;
minutes = self.inner.minute() as i64;
seconds = self.inner.second() as i64;
}
MilliSecond | Milli => {
year = self.inner.year();
month = self.inner.month() as i32;
day = self.inner.day() as i64;
hour = self.inner.hour() as i64;
minutes = self.inner.minute() as i64;
seconds = self.inner.second() as i64;
nano_secs = (self.inner.nanosecond() / 1_000_000 * 1_000_000) as i64;
}
MicroSecond | Micro => {
year = self.inner.year();
month = self.inner.month() as i32;
day = self.inner.day() as i64;
hour = self.inner.hour() as i64;
minutes = self.inner.minute() as i64;
seconds = self.inner.second() as i64;
nano_secs = (self.inner.nanosecond() / 1_000 * 1_000) as i64;
}
};
let mut ret = Self::time_date(year, month, day, hour, minutes, seconds, nano_secs, offset)?;
// Means we have to adjust for the week
if week != 0 {
ret = ret.time_add_date(0, 0, ((week - 1) * 7) as i64)?;
}
Ok(ret)
}
pub fn round_duration(&self, d: Duration) -> Result<Self> {
Ok(Self {
inner: self.inner.duration_round(d.inner)?,
})
}
pub fn time_get(&self, field: TimeField) -> Value {
use TimeField::*;
match 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(3) 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(
self.inner.second() as f64 + (self.inner.nanosecond() as f64) / (1_000_000_000_f64),
),
MilliSecond | Milli => {
Value::from_integer((self.inner.nanosecond() / 1_000_000 % 1_000) as i64)
}
MicroSecond | Micro => {
Value::from_integer((self.inner.nanosecond() / 1_000 % 1_000_000) as i64)
}
NanoSecond | Nano => {
Value::from_integer((self.inner.nanosecond() % 1_000_000_000) as i64)
}
IsoYear => Value::from_integer(self.inner.iso_week().year() as i64),
IsoWeek => Value::from_integer(self.inner.iso_week().week() as i64),
IsoDow => Value::from_integer(self.inner.weekday().days_since(Weekday::Sun) as i64),
YearDay => Value::from_integer(self.inner.ordinal() as i64),
WeekDay => Value::from_integer(self.inner.weekday().num_days_from_sunday() as i64),
Epoch => Value::from_float(
self.inner.timestamp() as f64 + self.inner.nanosecond() as f64 / 1_000_000_000_f64,
),
}
}
}
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(TimeError::InvalidFormat)?
.to_utc(),
})
}
}
impl From<NaiveDateTime> for Time {
fn from(value: NaiveDateTime) -> Self {
Self {
inner: value.and_utc(),
}
}
}
impl From<Time> for NaiveDateTime {
fn from(value: Time) -> Self {
value.inner.naive_utc()
}
}
impl Sub for Time {
type Output = Duration;
fn sub(self, rhs: Self) -> Self::Output {
Duration {
inner: self.inner - rhs.inner,
}
}
}
impl From<i64> for Duration {
fn from(value: i64) -> Self {
Self {
inner: chrono::Duration::nanoseconds(value),
}
}
}
impl Deref for Duration {
type Target = chrono::Duration;
fn deref(&self) -> &Self::Target {
&self.inner
}
}

401
testing/time.test Executable file
View File

@@ -0,0 +1,401 @@
#!/usr/bin/env tclsh
set testdir [file dirname $argv0]
source $testdir/tester.tcl
do_execsql_test time_date {
SELECT time_fmt_iso(time_date(2011, 11, 18));
SELECT time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35));
SELECT time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35, 666777888));
SELECT time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35, 0, 3*3600));
SELECT time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35, 666777888, 3*3600));
} {
{2011-11-18T00:00:00Z}
{2011-11-18T15:56:35Z}
{2011-11-18T15:56:35.666777888Z}
{2011-11-18T12:56:35Z}
{2011-11-18T12:56:35.666777888Z}
}
do_execsql_test time_get_year {
SELECT time_get_year(time_date(2011, 11, 18));
SELECT time_get_year(time_date(1842, 11, 18));
SELECT time_get_year(time_date(-1000, 11, 18));
} {
{2011}
{1842}
{-1000}
}
do_execsql_test time_get_month {
SELECT time_get_month(time_date(2011, 12, 18));
SELECT time_get_month(time_date(1842, 1, 18));
SELECT time_get_month(time_date(-1000, 5, 18));
} {
{12}
{1}
{5}
}
do_execsql_test time_get_day {
SELECT time_get_day(time_date(2011, 12, 31));
SELECT time_get_day(time_date(1842, 1, 10));
SELECT time_get_day(time_date(-1000, 5, 25));
} {
{31}
{10}
{25}
}
do_execsql_test time_get_hour {
SELECT time_get_hour(time_date(2011, 12, 31, 10, 5, 30));
} {
{10}
}
do_execsql_test time_get_minute {
SELECT time_get_minute(time_date(2011, 12, 31, 10, 5, 30));
} {
{5}
}
do_execsql_test time_get_second {
SELECT time_get_second(time_date(2011, 12, 31, 10, 5, 30, 431295000));
} {
{30}
}
do_execsql_test time_get_nanosecond {
SELECT time_get_nano(time_date(2011, 12, 31, 10, 5, 30, 431295000));
} {
{431295000}
}
do_execsql_test time_get_weekday {
SELECT time_get_weekday(time_date(2011, 12, 31, 10, 5, 30, 431295000));
SELECT time_get_weekday(time_date(2012, 01, 01, 10, 5, 30, 431295000));
} {
{6}
{0}
}
do_execsql_test time_get_yearday {
SELECT time_get_yearday(time_date(2011, 12, 31, 10, 5, 30, 431295000));
SELECT time_get_yearday(time_date(2012, 01, 01, 10, 5, 30, 431295000));
} {
{365}
{1}
}
do_execsql_test time_get_isoyear {
SELECT time_get_isoyear(time_date(2011, 12, 31, 10, 5, 30, 431295000));
SELECT time_get_isoyear(time_date(2012, 01, 01, 10, 5, 30, 431295000));
} {
{2011}
{2011}
}
do_execsql_test time_get_isoweek {
SELECT time_get_isoweek(time_date(2011, 12, 31, 10, 5, 30, 431295000));
SELECT time_get_isoweek(time_date(2012, 01, 01, 10, 5, 30, 431295000));
SELECT time_get_isoweek(time_date(2012, 01, 10, 10, 5, 30, 431295000));
} {
{52}
{52}
{2}
}
do_execsql_test time_get {
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'millennium');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'century');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'decade');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'year');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'quarter');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'month');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'day');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'hour');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'minute');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'second');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'milli');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'millisecond');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'micro');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'microsecond');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'nano');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'nanosecond');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'isoyear');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'isoweek');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'isodow');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'yearday');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'weekday');
SELECT time_get(time_date(2024, 8, 6, 21, 22, 15, 431295000), 'epoch');
} {
{2}
{20}
{202}
{2024}
{3}
{8}
{6}
{21}
{22}
{15.431295}
{431}
{431}
{431295}
{431295}
{431295000}
{431295000}
{2024}
{32}
{2}
{219}
{2}
{1722979335.431295}
}
do_execsql_test time_unix {
SELECT time_fmt_iso(time_unix(1321631795));
SELECT time_fmt_iso(time_unix(1321631795, 666777888));
} {
{2011-11-18T15:56:35Z}
{2011-11-18T15:56:35.666777888Z}
}
do_execsql_test time_milli {
SELECT time_fmt_iso(time_milli(1321631795666));
} {
{2011-11-18T15:56:35.666000000Z}
}
do_execsql_test time_micro {
SELECT time_fmt_iso(time_micro(1321631795666777));
} {
{2011-11-18T15:56:35.666777000Z}
}
do_execsql_test time_nano {
SELECT time_fmt_iso(time_nano(1321631795666777888));
} {
{2011-11-18T15:56:35.666777888Z}
}
do_execsql_test time_to_milli {
SELECT time_fmt_iso(time_milli(time_to_milli(time_date(2025, 01, 01))));
} {
{2025-01-01T00:00:00Z}
}
do_execsql_test time_to_micro {
SELECT time_fmt_iso(time_micro(time_to_micro(time_date(2025, 01, 01))));
} {
{2025-01-01T00:00:00Z}
}
do_execsql_test time_to_nano {
SELECT time_fmt_iso(time_nano(time_to_nano(time_date(2025, 01, 01))));
} {
{2025-01-01T00:00:00Z}
}
do_execsql_test time_after {
SELECT time_after(time_date(2025, 10, 10), time_date(2011, 11, 18));
} {
{1}
}
do_execsql_test time_before {
SELECT time_before(time_date(2025, 10, 10), time_date(2011, 11, 18));
} {
{0}
}
do_execsql_test time_compare {
SELECT time_compare(time_date(2025, 10, 10), time_date(2011, 11, 18));
SELECT time_compare(time_date(2025, 10, 10), time_date(2026, 11, 18));
SELECT time_compare(time_date(2025, 10, 10), time_date(2025, 10, 10));
} {
{1}
{-1}
{0}
}
do_execsql_test time_add {
SELECT time_fmt_iso(time_add(time_date(2025, 01, 01), 24*dur_h()));
SELECT time_fmt_iso(time_add(time_date(2025, 01, 01), 60*dur_m()));
SELECT time_fmt_iso(time_add(time_date(2025, 01, 01), 5*dur_m()+30*dur_s()));
SELECT time_fmt_iso(date_add(time_date(2025, 01, 01), 24*dur_h()));
SELECT time_fmt_iso(date_add(time_date(2025, 01, 01), 60*dur_m()));
SELECT time_fmt_iso(date_add(time_date(2025, 01, 01), 5*dur_m()+30*dur_s()));
} {
{2025-01-02T00:00:00Z}
{2025-01-01T01:00:00Z}
{2025-01-01T00:05:30Z}
{2025-01-02T00:00:00Z}
{2025-01-01T01:00:00Z}
{2025-01-01T00:05:30Z}
}
do_execsql_test time_add_date {
SELECT time_fmt_date(time_add_date(time_date(2011, 11, 18), 5));
SELECT time_fmt_date(time_add_date(time_date(2011, 11, 18), 3, 5));
SELECT time_fmt_date(time_add_date(time_date(2011, 11, 18), 3, 5, -10));
} {
{2016-11-18}
{2015-04-18}
{2015-04-08}
}
do_execsql_test time_sub {
SELECT time_sub(time_date(2011, 11, 19), time_date(2011, 11, 18));
SELECT time_sub(
time_date(2011, 11, 18, 16, 56, 35),
time_date(2011, 11, 18, 15, 56, 35)
);
SELECT time_sub(time_unix(1321631795, 5000000), time_unix(1321631795, 0));
} {
{86400000000000}
{3600000000000}
{5000000}
}
# time_since and time_until functions rely on time_now so it makes testing for these functions near impossible
do_execsql_test time_sub {
select 'original = ' || time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35, 666777888));
select 'millennium = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 'millennium'));
select 'century = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 'century'));
select 'decade = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 'decade'));
select 'year = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 'year'));
select 'quarter = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 'quarter'));
select 'month = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 'month'));
select 'week = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 'week'));
select 'day = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 'day'));
select 'hour = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 'hour'));
select 'minute = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 'minute'));
select 'second = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 'second'));
select 'milli = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 'milli'));
select 'micro = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 'micro'));
select '12h = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 12*dur_h()));
select '1h = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), dur_h()));
select '30m = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 30*dur_m()));
select '1m = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), dur_m()));
select '30s = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), 30*dur_s()));
select '1s = ' || time_fmt_iso(time_trunc(time_date(2011, 11, 18, 15, 56, 35, 666777888), dur_s()));
} {
{original = 2011-11-18T15:56:35.666777888Z}
{millennium = 2000-01-01T00:00:00Z}
{century = 2000-01-01T00:00:00Z}
{decade = 2010-01-01T00:00:00Z}
{year = 2011-01-01T00:00:00Z}
{quarter = 2011-10-01T00:00:00Z}
{month = 2011-11-01T00:00:00Z}
{week = 2011-11-12T00:00:00Z}
{day = 2011-11-18T00:00:00Z}
{hour = 2011-11-18T15:00:00Z}
{minute = 2011-11-18T15:56:00Z}
{second = 2011-11-18T15:56:35Z}
{milli = 2011-11-18T15:56:35.666000000Z}
{micro = 2011-11-18T15:56:35.666777000Z}
{12h = 2011-11-18T12:00:00Z}
{1h = 2011-11-18T15:00:00Z}
{30m = 2011-11-18T15:30:00Z}
{1m = 2011-11-18T15:56:00Z}
{30s = 2011-11-18T15:56:30Z}
{1s = 2011-11-18T15:56:35Z}
}
do_execsql_test time_sub {
SELECT '12h = ' || time_fmt_iso(time_round(time_date(2011, 11, 18, 15, 56, 35, 666777888), 12*dur_h()));
SELECT '1h = ' || time_fmt_iso(time_round(time_date(2011, 11, 18, 15, 56, 35, 666777888), dur_h()));
SELECT '30m = ' || time_fmt_iso(time_round(time_date(2011, 11, 18, 15, 56, 35, 666777888), 30*dur_m()));
SELECT '1m = ' || time_fmt_iso(time_round(time_date(2011, 11, 18, 15, 56, 35, 666777888), dur_m()));
SELECT '30s = ' || time_fmt_iso(time_round(time_date(2011, 11, 18, 15, 56, 35, 666777888), 30*dur_s()));
SELECT '1s = ' || time_fmt_iso(time_round(time_date(2011, 11, 18, 15, 56, 35, 666777888), dur_s()));
} {
{12h = 2011-11-18T12:00:00Z}
{1h = 2011-11-18T16:00:00Z}
{30m = 2011-11-18T16:00:00Z}
{1m = 2011-11-18T15:57:00Z}
{30s = 2011-11-18T15:56:30Z}
{1s = 2011-11-18T15:56:36Z}
}
do_execsql_test time_fmt_iso {
SELECT time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35, 666777888), 3*3600);
SELECT time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35, 666777888));
SELECT time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35), 3*3600);
SELECT time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35));
} {
{2011-11-18T18:56:35.666777888+03:00}
{2011-11-18T15:56:35.666777888Z}
{2011-11-18T18:56:35+03:00}
{2011-11-18T15:56:35Z}
}
do_execsql_test time_fmt_datetime {
SELECT time_fmt_datetime(time_date(2011, 11, 18, 15, 56, 35), 3*3600);
SELECT time_fmt_datetime(time_date(2011, 11, 18, 15, 56, 35));
SELECT time_fmt_datetime(time_date(2011, 11, 18));
} {
{2011-11-18 18:56:35}
{2011-11-18 15:56:35}
{2011-11-18 00:00:00}
}
do_execsql_test time_fmt_date {
SELECT time_fmt_date(time_date(2011, 11, 18, 15, 56, 35), 12*3600);
SELECT time_fmt_date(time_date(2011, 11, 18, 15, 56, 35));
SELECT time_fmt_date(time_date(2011, 11, 18));
} {
{2011-11-19}
{2011-11-18}
{2011-11-18}
}
do_execsql_test time_fmt_time {
SELECT time_fmt_time(time_date(2011, 11, 18, 15, 56, 35), 3*3600);
SELECT time_fmt_time(time_date(2011, 11, 18, 15, 56, 35));
SELECT time_fmt_time(time_date(2011, 11, 18));
} {
{18:56:35}
{15:56:35}
{00:00:00}
}
do_execsql_test time_parse {
SELECT time_parse('2011-11-18T15:56:35.666777888Z') = time_unix(1321631795, 666777888);
SELECT time_parse('2011-11-18T19:26:35.666777888+03:30') = time_unix(1321631795, 666777888);
SELECT time_parse('2011-11-18T12:26:35.666777888-03:30') = time_unix(1321631795, 666777888);
SELECT time_parse('2011-11-18T15:56:35Z') = time_unix(1321631795, 0);
SELECT time_parse('2011-11-18T19:26:35+03:30') = time_unix(1321631795, 0);
SELECT time_parse('2011-11-18T12:26:35-03:30') = time_unix(1321631795, 0);
SELECT time_parse('2011-11-18 15:56:35') = time_unix(1321631795, 0);
SELECT time_parse('2011-11-18') = time_date(2011, 11, 18);
SELECT time_parse('15:56:35') = time_date(1, 1, 1, 15, 56, 35);
} {
{1}
{1}
{1}
{1}
{1}
{1}
{1}
{1}
{1}
}
do_execsql_test duration_constants {
SELECT dur_ns();
SELECT dur_us();
SELECT dur_ms();
SELECT dur_s();
SELECT dur_m();
SELECT dur_h();
} {
{1}
{1000}
{1000000}
{1000000000}
{60000000000}
{3600000000000}
}