diff --git a/docs/docs/core/data_types.mdx b/docs/docs/core/data_types.mdx index 2b38a84..a9546c1 100644 --- a/docs/docs/core/data_types.mdx +++ b/docs/docs/core/data_types.mdx @@ -29,6 +29,7 @@ This is the list of all basic types supported by CocoIndex: | Time | | `datetime.time` | `datetime.time` | | LocalDatetime | Date and time without timezone | `cocoindex.typing.LocalDateTime` | `datetime.datetime` | | OffsetDatetime | Date and time with a timezone offset | `cocoindex.typing.OffsetDateTime` | `datetime.datetime` | +| TimeDelta | A duration of time | `cocoindex.typing.TimeDelta` | `datetime.timedelta` | | Vector[*type*, *N*?] | |`Annotated[list[type], cocoindex.typing.Vector(dim=N)]` | `list[type]` | | Json | | `cocoindex.typing.Json` | Any type convertible to JSON by `json` package | diff --git a/python/cocoindex/typing.py b/python/cocoindex/typing.py index 1c6382a..04d91cc 100644 --- a/python/cocoindex/typing.py +++ b/python/cocoindex/typing.py @@ -29,6 +29,7 @@ def __init__(self, key: str, value: Any): Json = Annotated[Any, TypeKind('Json')] LocalDateTime = Annotated[datetime.datetime, TypeKind('LocalDateTime')] OffsetDateTime = Annotated[datetime.datetime, TypeKind('OffsetDateTime')] +TimeDelta = Annotated[datetime.timedelta, TypeKind('TimeDelta')] COLLECTION_TYPES = ('Table', 'List') @@ -142,6 +143,8 @@ def analyze_type_info(t) -> AnalyzedTypeInfo: kind = 'Time' elif t is datetime.datetime: kind = 'OffsetDateTime' + elif t is datetime.timedelta: + kind = 'TimeDelta' else: raise ValueError(f"type unsupported yet: {t}") diff --git a/src/base/schema.rs b/src/base/schema.rs index b458879..2b4b497 100644 --- a/src/base/schema.rs +++ b/src/base/schema.rs @@ -50,6 +50,9 @@ pub enum BasicValueType { /// Date and time with timezone. OffsetDateTime, + /// A time duration. + TimeDelta, + /// A JSON value. Json, @@ -72,6 +75,7 @@ impl std::fmt::Display for BasicValueType { BasicValueType::Time => write!(f, "time"), BasicValueType::LocalDateTime => write!(f, "local_datetime"), BasicValueType::OffsetDateTime => write!(f, "offset_datetime"), + BasicValueType::TimeDelta => write!(f, "timedelta"), BasicValueType::Json => write!(f, "json"), BasicValueType::Vector(s) => write!( f, diff --git a/src/base/value.rs b/src/base/value.rs index 9833970..cc0d880 100644 --- a/src/base/value.rs +++ b/src/base/value.rs @@ -5,6 +5,7 @@ use anyhow::Result; use base64::prelude::*; use chrono::Offset; use log::warn; +use pyo3::pyclass; use serde::{ de::{SeqAccess, Visitor}, ser::{SerializeMap, SerializeSeq, SerializeTuple}, @@ -354,6 +355,7 @@ pub enum BasicValue { Time(chrono::NaiveTime), LocalDateTime(chrono::NaiveDateTime), OffsetDateTime(chrono::DateTime), + TimeDelta(chrono::Duration), Json(Arc), Vector(Arc<[BasicValue]>), } @@ -436,6 +438,12 @@ impl From> for BasicValue { } } +impl From for BasicValue { + fn from(value: chrono::Duration) -> Self { + BasicValue::TimeDelta(value) + } +} + impl From for BasicValue { fn from(value: serde_json::Value) -> Self { BasicValue::Json(Arc::from(value)) @@ -465,6 +473,7 @@ impl BasicValue { | BasicValue::Time(_) | BasicValue::LocalDateTime(_) | BasicValue::OffsetDateTime(_) + | BasicValue::TimeDelta(_) | BasicValue::Json(_) | BasicValue::Vector(_) => api_bail!("invalid key value type"), }; @@ -485,6 +494,7 @@ impl BasicValue { | BasicValue::Time(_) | BasicValue::LocalDateTime(_) | BasicValue::OffsetDateTime(_) + | BasicValue::TimeDelta(_) | BasicValue::Json(_) | BasicValue::Vector(_) => api_bail!("invalid key value type"), }; @@ -505,6 +515,7 @@ impl BasicValue { BasicValue::Time(_) => "time", BasicValue::LocalDateTime(_) => "local_datetime", BasicValue::OffsetDateTime(_) => "offset_datetime", + BasicValue::TimeDelta(_) => "timedelta", BasicValue::Json(_) => "json", BasicValue::Vector(_) => "vector", } @@ -860,6 +871,7 @@ impl serde::Serialize for BasicValue { BasicValue::OffsetDateTime(v) => { serializer.serialize_str(&v.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true)) } + BasicValue::TimeDelta(v) => serializer.serialize_str(&v.to_string()), BasicValue::Json(v) => v.serialize(serializer), BasicValue::Vector(v) => v.serialize(serializer), } @@ -912,6 +924,7 @@ impl BasicValue { } } } + (v, BasicValueType::TimeDelta) => BasicValue::TimeDelta(v.as_duration()?), (v, BasicValueType::Json) => BasicValue::Json(Arc::from(v)), ( serde_json::Value::Array(v), diff --git a/src/ops/storages/postgres.rs b/src/ops/storages/postgres.rs index 3af2e65..35da298 100644 --- a/src/ops/storages/postgres.rs +++ b/src/ops/storages/postgres.rs @@ -134,6 +134,9 @@ fn bind_value_field<'arg>( BasicValue::OffsetDateTime(v) => { builder.push_bind(v); } + BasicValue::TimeDelta(v) => { + builder.push_bind(v); + } BasicValue::Json(v) => { builder.push_bind(sqlx::types::Json(&**v)); } @@ -219,6 +222,9 @@ fn from_pg_value(row: &PgRow, field_idx: usize, typ: &ValueType) -> Result row .try_get::>, _>(field_idx)? .map(BasicValue::OffsetDateTime), + BasicValueType::TimeDelta => row + .try_get::, _>(field_idx)? + .map(BasicValue::TimeDelta), BasicValueType::Json => row .try_get::, _>(field_idx)? .map(|v| BasicValue::Json(Arc::from(v))), @@ -701,6 +707,7 @@ fn to_column_type_sql(column_type: &ValueType) -> Cow<'static, str> { BasicValueType::Time => "time".into(), BasicValueType::LocalDateTime => "timestamp".into(), BasicValueType::OffsetDateTime => "timestamp with time zone".into(), + BasicValueType::TimeDelta => "interval".into(), BasicValueType::Json => "jsonb".into(), BasicValueType::Vector(vec_schema) => { if convertible_to_pgvector(vec_schema) { diff --git a/src/py/convert.rs b/src/py/convert.rs index 6410f1a..eba53d5 100644 --- a/src/py/convert.rs +++ b/src/py/convert.rs @@ -1,5 +1,6 @@ use bytes::Bytes; use pyo3::types::{PyList, PyTuple}; +use pyo3::types::{PyDateAccess, PyDateTime, PyDelta, PyTimeAccess, PyTzInfoAccess}; use pyo3::IntoPyObjectExt; use pyo3::{exceptions::PyException, prelude::*}; use pythonize::{depythonize, pythonize}; @@ -70,6 +71,9 @@ fn basic_value_to_py_object<'py>( value::BasicValue::Time(v) => v.into_bound_py_any(py)?, value::BasicValue::LocalDateTime(v) => v.into_bound_py_any(py)?, value::BasicValue::OffsetDateTime(v) => v.into_bound_py_any(py)?, + value::BasicValue::TimeDelta(v) => { + PyDelta::new(py, 0, v.num_seconds() as i32, v.subsec_micros())?.into_bound_py_any(py)? + } value::BasicValue::Json(v) => pythonize(py, v).into_py_result()?, value::BasicValue::Vector(v) => v .iter() @@ -143,6 +147,13 @@ fn basic_value_from_py_object<'py>( schema::BasicValueType::OffsetDateTime => { value::BasicValue::OffsetDateTime(v.extract::>()?) } + schema::BasicValueType::TimeDelta => { + let delta = v.extract::<&PyDelta>()?; + value::BasicValue::TimeDelta( + chrono::Duration::seconds(delta.get_days() as i64 * 86400 + delta.get_seconds() as i64) + + chrono::Duration::microseconds(delta.get_microseconds() as i64), + ) + } schema::BasicValueType::Json => { value::BasicValue::Json(Arc::from(depythonize::(v)?)) }