Skip to content

Commit afee760

Browse files
authored
feat: use jiff::Zoned in Value::Timestamp (#718)
* feat: use jiff::Zoned in Value::Timestamp * bindings convert from jiff directly * fix py38
1 parent b4c5d82 commit afee760

File tree

15 files changed

+141
-63
lines changed

15 files changed

+141
-63
lines changed

bindings/nodejs/src/lib.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
#[macro_use]
1616
extern crate napi_derive;
1717

18-
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime};
18+
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
1919
use databend_driver::LoadMethod;
2020
use napi::{bindgen_prelude::*, Env};
2121
use once_cell::sync::Lazy;
@@ -316,9 +316,17 @@ impl ToNapiValue for Value<'_> {
316316
databend_driver::Value::Number(n) => {
317317
NumberValue::to_napi_value(env, NumberValue(n.clone()))
318318
}
319-
databend_driver::Value::Timestamp(dt) => DateTime::to_napi_value(env, *dt),
319+
databend_driver::Value::Timestamp(dt) => {
320+
let mut js_date = std::ptr::null_mut();
321+
let millis = dt.timestamp().as_millisecond() as f64;
322+
check_status!(
323+
unsafe { sys::napi_create_date(env, millis, &mut js_date) },
324+
"Failed to convert jiff timestamp into napi value",
325+
)?;
326+
Ok(js_date)
327+
}
320328
databend_driver::Value::TimestampTz(dt) => {
321-
let formatted = dt.format(TIMESTAMP_TIMEZONE_FORMAT);
329+
let formatted = dt.strftime(TIMESTAMP_TIMEZONE_FORMAT);
322330
String::to_napi_value(env, formatted.to_string())
323331
}
324332
databend_driver::Value::Date(_) => {

bindings/python/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ ctor = "0.2"
2323
env_logger = "0.11.8"
2424
http = "1.0"
2525
once_cell = "1.21"
26-
pyo3 = { version = "0.24.2", features = ["extension-module", "chrono", "chrono-tz"] }
26+
pyo3 = { version = "0.24.2", features = ["extension-module", "chrono", "chrono-tz", "jiff-02"] }
2727
pyo3-async-runtimes = { version = "0.24", features = ["tokio-runtime"] }
2828
tokio = "1.44"
2929

bindings/python/src/types.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ use tokio_stream::StreamExt;
3131

3232
use crate::exceptions::map_error_to_exception;
3333
use crate::utils::wait_for_future;
34+
#[cfg(feature = "cp38")]
35+
use databend_driver::{self, zoned_to_chrono_datetime, zoned_to_chrono_fixed_offset};
3436

3537
pub static VERSION: Lazy<String> = Lazy::new(|| {
3638
let version = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown");
@@ -80,15 +82,31 @@ impl<'py> IntoPyObject<'py> for Value {
8082
databend_driver::Value::Timestamp(dt) => {
8183
#[cfg(feature = "cp38")]
8284
{
83-
// chrono_tz -> PyDateTime isn't implemented for Python < 3.9 (no zoneinfo).
84-
dt.with_timezone(&dt.offset().fix()).into_bound_py_any(py)?
85+
let chrono_dt = zoned_to_chrono_datetime(&dt).map_err(|e| {
86+
PyException::new_err(format!("failed to convert timestamp: {e}"))
87+
})?;
88+
chrono_dt
89+
.with_timezone(&chrono_dt.offset().fix())
90+
.into_bound_py_any(py)?
8591
}
8692
#[cfg(not(feature = "cp38"))]
8793
{
8894
dt.into_bound_py_any(py)?
8995
}
9096
}
91-
databend_driver::Value::TimestampTz(t) => t.into_bound_py_any(py)?,
97+
databend_driver::Value::TimestampTz(t) => {
98+
#[cfg(feature = "cp38")]
99+
{
100+
let chrono_dt = zoned_to_chrono_fixed_offset(&t).map_err(|e| {
101+
PyException::new_err(format!("failed to convert timestamp_tz: {e}"))
102+
})?;
103+
chrono_dt.into_bound_py_any(py)?
104+
}
105+
#[cfg(not(feature = "cp38"))]
106+
{
107+
t.into_bound_py_any(py)?
108+
}
109+
}
92110
databend_driver::Value::Date(_) => {
93111
let d = NaiveDate::try_from(self.0)
94112
.map_err(|e| PyException::new_err(format!("failed to convert date: {e}")))?;

driver/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ pub use databend_driver_core::rows::{
3939
Row, RowIterator, RowStatsIterator, RowWithStats, ServerStats,
4040
};
4141
pub use databend_driver_core::value::Interval;
42-
pub use databend_driver_core::value::{NumberValue, Value};
42+
pub use databend_driver_core::value::{
43+
zoned_to_chrono_datetime, zoned_to_chrono_fixed_offset, NumberValue, Value,
44+
};
4345

4446
pub use databend_driver_macros::serde_bend;
4547
pub use databend_driver_macros::TryFromRow;

sql/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ itertools = "0.14"
3333
lexical-core = "1.0.5"
3434
memchr = "2.7"
3535
roaring = { version = "0.10.12", features = ["serde"] }
36+
jiff = "0.2.10"
3637
serde = { version = "1.0", default-features = false, features = ["derive"] }
3738
serde_json = { version = "1.0", default-features = false, features = ["std"] }
3839
url = { version = "2.5", default-features = false }

sql/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,12 @@ impl From<serde_json::Error> for Error {
174174
}
175175
}
176176

177+
impl From<jiff::Error> for Error {
178+
fn from(e: jiff::Error) -> Self {
179+
Error::Parsing(e.to_string())
180+
}
181+
}
182+
177183
impl From<hex::FromHexError> for Error {
178184
fn from(e: hex::FromHexError) -> Self {
179185
Error::Parsing(e.to_string())

sql/src/value/arrow_decoder.rs

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ use arrow_array::{
2424
StructArray, TimestampMicrosecondArray, UInt16Array, UInt32Array, UInt64Array, UInt8Array,
2525
};
2626
use arrow_schema::{DataType as ArrowDataType, Field as ArrowField, TimeUnit};
27-
use chrono::{FixedOffset, LocalResult, TimeZone};
2827
use databend_client::schema::{
2928
DecimalSize, ARROW_EXT_TYPE_BITMAP, ARROW_EXT_TYPE_EMPTY_ARRAY, ARROW_EXT_TYPE_EMPTY_MAP,
3029
ARROW_EXT_TYPE_GEOGRAPHY, ARROW_EXT_TYPE_GEOMETRY, ARROW_EXT_TYPE_INTERVAL,
@@ -33,6 +32,7 @@ use databend_client::schema::{
3332
};
3433
use databend_client::ResultFormatSettings;
3534
use ethnum::i256;
35+
use jiff::{tz, Timestamp};
3636
use jsonb::RawJsonb;
3737

3838
/// The in-memory representation of the MonthDayMicros variant of the "Interval" logical type.
@@ -103,15 +103,14 @@ impl
103103
let v = array.value(seq);
104104
let unix_ts = v as u64 as i64;
105105
let offset = (v >> 64) as i32;
106-
let offset = FixedOffset::east_opt(offset)
107-
.ok_or_else(|| Error::Parsing("invalid offset".to_string()))?;
108-
let dt =
109-
offset.timestamp_micros(unix_ts).single().ok_or_else(|| {
110-
Error::Parsing(format!(
111-
"Invalid timestamp_micros {unix_ts} for offset {offset}"
112-
))
113-
})?;
114-
Ok(Value::TimestampTz(dt))
106+
let offset = tz::Offset::from_seconds(offset).map_err(|e| {
107+
Error::Parsing(format!("invalid offset: {offset}, {e}"))
108+
})?;
109+
let time_zone = tz::TimeZone::fixed(offset);
110+
let timestamp = Timestamp::from_microsecond(unix_ts).map_err(|e| {
111+
Error::Parsing(format!("Invalid timestamp_micros {unix_ts}: {e}"))
112+
})?;
113+
Ok(Value::TimestampTz(timestamp.to_zoned(time_zone)))
115114
}
116115
None => Err(ConvertError::new("Interval", format!("{array:?}")).into()),
117116
}
@@ -347,16 +346,15 @@ impl
347346
let ts = array.value(seq);
348347
match tz {
349348
None => {
350-
let ltz = settings.timezone;
351-
let dt = match ltz.timestamp_micros(ts) {
352-
LocalResult::Single(dt) => dt,
353-
LocalResult::None => {
354-
return Err(Error::Parsing(format!(
355-
"time {ts} not exists in timezone {ltz}"
356-
)))
357-
}
358-
LocalResult::Ambiguous(dt1, _dt2) => dt1,
359-
};
349+
let timestamp = Timestamp::from_microsecond(ts).map_err(|e| {
350+
Error::Parsing(format!("Invalid timestamp_micros {ts}: {e}"))
351+
})?;
352+
let tz_name = settings.timezone.name();
353+
let dt = timestamp.in_tz(tz_name).map_err(|e| {
354+
Error::Parsing(format!(
355+
"Invalid timezone {tz_name} for timestamp {ts}: {e}"
356+
))
357+
})?;
360358
Ok(Value::Timestamp(dt))
361359
}
362360
Some(tz) => Err(ConvertError::new("timestamp", format!("{array:?}"))

sql/src/value/base.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use chrono::{DateTime, FixedOffset};
16-
use chrono_tz::Tz;
1715
use databend_client::schema::{DataType, DecimalDataType, DecimalSize, NumberDataType};
1816
use ethnum::i256;
17+
use jiff::Zoned;
1918

2019
// Thu 1970-01-01 is R.D. 719163
2120
pub(crate) const DAYS_FROM_CE: i32 = 719_163;
@@ -48,8 +47,8 @@ pub enum Value {
4847
String(String),
4948
Number(NumberValue),
5049
/// Microseconds from 1970-01-01 00:00:00 UTC
51-
Timestamp(DateTime<Tz>),
52-
TimestampTz(DateTime<FixedOffset>),
50+
Timestamp(Zoned),
51+
TimestampTz(Zoned),
5352
Date(i32),
5453
Array(Vec<Value>),
5554
Map(Vec<(Value, Value)>),

sql/src/value/convert.rs

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, TimeZone};
15+
use chrono::{
16+
DateTime, Datelike, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, TimeZone, Utc,
17+
};
1618
use chrono_tz::Tz;
1719
use std::collections::HashMap;
1820
use std::hash::Hash;
1921

2022
use crate::error::{ConvertError, Error, Result};
2123

2224
use super::{NumberValue, Value, DAYS_FROM_CE};
25+
use jiff::{tz::TimeZone as JiffTimeZone, Timestamp, Zoned};
2326

2427
impl TryFrom<Value> for bool {
2528
type Error = Error;
@@ -70,11 +73,62 @@ impl_try_from_number_value!(i64);
7073
impl_try_from_number_value!(f32);
7174
impl_try_from_number_value!(f64);
7275

76+
fn unix_micros_from_zoned(zdt: &Zoned) -> i64 {
77+
zdt.timestamp().as_microsecond()
78+
}
79+
80+
fn naive_datetime_from_micros(micros: i64) -> Result<NaiveDateTime> {
81+
DateTime::<Utc>::from_timestamp_micros(micros)
82+
.map(|dt| dt.naive_utc())
83+
.ok_or_else(|| Error::Parsing(format!("invalid unix timestamp {micros}")))
84+
}
85+
86+
pub fn zoned_to_chrono_datetime(zdt: &Zoned) -> Result<DateTime<Tz>> {
87+
let tz_name = zdt.time_zone().iana_name().ok_or_else(|| {
88+
ConvertError::new(
89+
"DateTime",
90+
"timestamp does not contain an IANA time zone".to_string(),
91+
)
92+
})?;
93+
let tz: Tz = tz_name.parse().map_err(|_| {
94+
ConvertError::new(
95+
"DateTime",
96+
format!("invalid time zone identifier {tz_name}"),
97+
)
98+
})?;
99+
let micros = unix_micros_from_zoned(zdt);
100+
match tz.timestamp_micros(micros) {
101+
LocalResult::Single(dt) => Ok(dt),
102+
LocalResult::Ambiguous(dt, _) => Ok(dt),
103+
LocalResult::None => Err(Error::Parsing(format!(
104+
"time {micros} not exists in timezone {tz_name}"
105+
))),
106+
}
107+
}
108+
109+
pub fn zoned_to_chrono_fixed_offset(zdt: &Zoned) -> Result<DateTime<FixedOffset>> {
110+
let offset_seconds = zdt.offset().seconds();
111+
let offset = FixedOffset::east_opt(offset_seconds)
112+
.ok_or_else(|| Error::Parsing(format!("invalid offset {offset_seconds}")))?;
113+
let micros = unix_micros_from_zoned(zdt);
114+
let naive = naive_datetime_from_micros(micros)?;
115+
Ok(DateTime::<FixedOffset>::from_naive_utc_and_offset(
116+
naive, offset,
117+
))
118+
}
119+
120+
fn zoned_from_naive_datetime(naive_dt: &NaiveDateTime) -> Zoned {
121+
let micros = naive_dt.and_utc().timestamp_micros();
122+
let timestamp = Timestamp::from_microsecond(micros)
123+
.expect("NaiveDateTime out of range for Timestamp conversion");
124+
timestamp.to_zoned(JiffTimeZone::UTC)
125+
}
126+
73127
impl TryFrom<Value> for NaiveDateTime {
74128
type Error = Error;
75129
fn try_from(val: Value) -> Result<Self> {
76130
match val {
77-
Value::Timestamp(dt) => Ok(dt.naive_utc()),
131+
Value::Timestamp(dt) => naive_datetime_from_micros(unix_micros_from_zoned(&dt)),
78132
_ => Err(ConvertError::new("NaiveDateTime", format!("{val}")).into()),
79133
}
80134
}
@@ -84,7 +138,7 @@ impl TryFrom<Value> for DateTime<Tz> {
84138
type Error = Error;
85139
fn try_from(val: Value) -> Result<Self> {
86140
match val {
87-
Value::Timestamp(dt) => Ok(dt),
141+
Value::Timestamp(dt) => zoned_to_chrono_datetime(&dt),
88142
_ => Err(ConvertError::new("DateTime", format!("{val}")).into()),
89143
}
90144
}
@@ -426,15 +480,13 @@ impl From<&NaiveDate> for Value {
426480

427481
impl From<NaiveDateTime> for Value {
428482
fn from(naive_dt: NaiveDateTime) -> Self {
429-
let dt = Tz::UTC.from_local_datetime(&naive_dt).unwrap();
430-
Value::Timestamp(dt)
483+
Value::Timestamp(zoned_from_naive_datetime(&naive_dt))
431484
}
432485
}
433486

434487
impl From<&NaiveDateTime> for Value {
435488
fn from(naive_dt: &NaiveDateTime) -> Self {
436-
let dt = Tz::UTC.from_local_datetime(naive_dt).unwrap();
437-
Value::Timestamp(dt)
489+
Value::Timestamp(zoned_from_naive_datetime(naive_dt))
438490
}
439491
}
440492

sql/src/value/format/display.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,15 @@ impl Value {
7474
}
7575
}
7676
Value::Timestamp(dt) => {
77-
let formatted = dt.format(TIMESTAMP_FORMAT);
77+
let formatted = dt.strftime(TIMESTAMP_FORMAT);
7878
if raw {
7979
write!(f, "{formatted}")
8080
} else {
8181
write!(f, "'{formatted}'")
8282
}
8383
}
8484
Value::TimestampTz(dt) => {
85-
let formatted = dt.format(TIMESTAMP_TIMEZONE_FORMAT);
85+
let formatted = dt.strftime(TIMESTAMP_TIMEZONE_FORMAT);
8686
if raw {
8787
write!(f, "{formatted}")
8888
} else {

0 commit comments

Comments
 (0)