From aead19e73a4d21bf953b7296e1fc6d4a987d338e Mon Sep 17 00:00:00 2001 From: Yang Xiufeng Date: Tue, 11 Nov 2025 22:30:59 +0800 Subject: [PATCH 1/6] fix timestampTz --- bindings/nodejs/src/lib.rs | 4 +- bindings/python/src/types.rs | 2 +- sql/Cargo.toml | 1 + sql/src/schema.rs | 2 + sql/src/value.rs | 71 +++++++++++++++++++++++++++++------- 5 files changed, 64 insertions(+), 16 deletions(-) diff --git a/bindings/nodejs/src/lib.rs b/bindings/nodejs/src/lib.rs index 49bda719..999bd7a0 100644 --- a/bindings/nodejs/src/lib.rs +++ b/bindings/nodejs/src/lib.rs @@ -359,7 +359,9 @@ impl ToNapiValue for Value<'_> { String::to_napi_value(env, s.to_string()) } } - databend_driver::Value::TimestampTz(s) => String::to_napi_value(env, s.to_string()), + databend_driver::Value::TimestampTzString(s) => { + String::to_napi_value(env, s.to_string()) + } databend_driver::Value::Geometry(s) => String::to_napi_value(env, s.to_string()), databend_driver::Value::Interval(s) => String::to_napi_value(env, s.to_string()), databend_driver::Value::Geography(s) => String::to_napi_value(env, s.to_string()), diff --git a/bindings/python/src/types.rs b/bindings/python/src/types.rs index 47b2174b..537d18be 100644 --- a/bindings/python/src/types.rs +++ b/bindings/python/src/types.rs @@ -113,7 +113,7 @@ impl<'py> IntoPyObject<'py> for Value { let tuple = PyTuple::new(py, inner.into_iter().map(Value))?; tuple.into_bound_py_any(py)? } - databend_driver::Value::TimestampTz(s) => s.into_bound_py_any(py)?, + databend_driver::Value::TimestampTzString(s) => s.into_bound_py_any(py)?, databend_driver::Value::Bitmap(s) => s.into_bound_py_any(py)?, databend_driver::Value::Variant(s) => s.into_bound_py_any(py)?, databend_driver::Value::Geometry(s) => s.into_bound_py_any(py)?, diff --git a/sql/Cargo.toml b/sql/Cargo.toml index 6b3936bc..0e0bcc9d 100644 --- a/sql/Cargo.toml +++ b/sql/Cargo.toml @@ -22,6 +22,7 @@ chrono = { workspace = true } chrono-tz = { workspace = true } databend-client = { workspace = true } jsonb = { workspace = true } +jiff = "0.2.10" tokio-stream = { workspace = true } tonic = { workspace = true, optional = true } diff --git a/sql/src/schema.rs b/sql/src/schema.rs index 96c35e63..7874fbdf 100644 --- a/sql/src/schema.rs +++ b/sql/src/schema.rs @@ -30,6 +30,7 @@ pub(crate) const ARROW_EXT_TYPE_GEOMETRY: &str = "Geometry"; pub(crate) const ARROW_EXT_TYPE_GEOGRAPHY: &str = "Geography"; pub(crate) const ARROW_EXT_TYPE_INTERVAL: &str = "Interval"; pub(crate) const ARROW_EXT_TYPE_VECTOR: &str = "Vector"; +pub(crate) const ARROW_EXT_TYPE_TIMESTAMP_TIMEZONE: &str = "TimestampTz"; #[derive(Debug, Clone, PartialEq, Eq)] pub enum NumberDataType { @@ -321,6 +322,7 @@ impl TryFrom<&Arc> for Field { ARROW_EXT_TYPE_GEOMETRY => DataType::Geometry, ARROW_EXT_TYPE_GEOGRAPHY => DataType::Geography, ARROW_EXT_TYPE_INTERVAL => DataType::Interval, + ARROW_EXT_TYPE_TIMESTAMP_TIMEZONE => DataType::TimestampTz, ARROW_EXT_TYPE_VECTOR => match f.data_type() { ArrowDataType::FixedSizeList(field, dimension) => { let dimension = match field.data_type() { diff --git a/sql/src/value.rs b/sql/src/value.rs index 2a04202b..a326e68e 100644 --- a/sql/src/value.rs +++ b/sql/src/value.rs @@ -12,6 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::cursor_ext::{ + collect_binary_number, collect_number, BufferReadStringExt, ReadBytesExt, ReadCheckPointExt, + ReadNumberExt, +}; +use crate::error::{ConvertError, Error, Result}; +use crate::schema::{DataType, DecimalDataType, DecimalSize, NumberDataType}; use arrow_buffer::i256; use chrono::{DateTime, Datelike, LocalResult, NaiveDate, NaiveDateTime, TimeZone}; use chrono_tz::Tz; @@ -19,24 +25,20 @@ use geozero::wkb::FromWkb; use geozero::wkb::WkbDialect; use geozero::wkt::Ewkt; use hex; +use jiff::fmt::strtime; +use jiff::{tz, Timestamp}; use std::collections::HashMap; use std::fmt::{Display, Formatter, Write}; use std::hash::Hash; use std::io::BufRead; use std::io::Cursor; -use crate::cursor_ext::{ - collect_binary_number, collect_number, BufferReadStringExt, ReadBytesExt, ReadCheckPointExt, - ReadNumberExt, -}; -use crate::error::{ConvertError, Error, Result}; -use crate::schema::{DataType, DecimalDataType, DecimalSize, NumberDataType}; - use { crate::schema::{ ARROW_EXT_TYPE_BITMAP, ARROW_EXT_TYPE_EMPTY_ARRAY, ARROW_EXT_TYPE_EMPTY_MAP, ARROW_EXT_TYPE_GEOGRAPHY, ARROW_EXT_TYPE_GEOMETRY, ARROW_EXT_TYPE_INTERVAL, - ARROW_EXT_TYPE_VARIANT, ARROW_EXT_TYPE_VECTOR, EXTENSION_KEY, + ARROW_EXT_TYPE_TIMESTAMP_TIMEZONE, ARROW_EXT_TYPE_VARIANT, ARROW_EXT_TYPE_VECTOR, + EXTENSION_KEY, }, arrow_array::{ Array as ArrowArray, BinaryArray, BooleanArray, Date32Array, Decimal128Array, @@ -56,6 +58,7 @@ const NULL_VALUE: &str = "NULL"; const TRUE_VALUE: &str = "1"; const FALSE_VALUE: &str = "0"; const TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S%.6f"; +const TIMESTAMP_TIMEZONE_FORMAT: &str = "%Y-%m-%d %H:%M:%S%.6f %z"; #[derive(Clone, Debug, PartialEq)] pub enum NumberValue { @@ -84,7 +87,8 @@ pub enum Value { Number(NumberValue), /// Microseconds from 1970-01-01 00:00:00 UTC Timestamp(i64, Tz), - TimestampTz(String), + TimestampTz(i64, i32), + TimestampTzString(String), Date(i32), Array(Vec), Map(Vec<(Value, Value)>), @@ -121,7 +125,8 @@ impl Value { NumberValue::Decimal256(_, s) => DataType::Decimal(DecimalDataType::Decimal256(*s)), }, Self::Timestamp(_, _) => DataType::Timestamp, - Self::TimestampTz(_) => DataType::TimestampTz, + Self::TimestampTz(_, _) => DataType::TimestampTz, + Self::TimestampTzString(_) => DataType::TimestampTz, Self::Date(_) => DataType::Date, Self::Interval(_) => DataType::Interval, @@ -233,7 +238,7 @@ impl TryFrom<(&DataType, String, Tz)> for Value { let ts = dt_with_tz.timestamp_micros(); Ok(Self::Timestamp(ts, tz)) } - DataType::TimestampTz => Ok(Self::TimestampTz(v)), + DataType::TimestampTz => Ok(Self::TimestampTzString(v)), DataType::Date => Ok(Self::Date( NaiveDate::parse_from_str(v.as_str(), "%Y-%m-%d")?.num_days_from_ce() - DAYS_FROM_CE, @@ -284,6 +289,20 @@ impl TryFrom<(&ArrowField, &Arc, usize, Tz)> for Value { None => Err(ConvertError::new("variant", format!("{array:?}")).into()), } } + ARROW_EXT_TYPE_TIMESTAMP_TIMEZONE => { + if field.is_nullable() && array.is_null(seq) { + return Ok(Value::Null); + } + match array.as_any().downcast_ref::() { + Some(array) => { + let v = array.value(seq); + let ts = v as u64 as i64; + let offset = (v >> 64) as i32; + Ok(Value::TimestampTz(ts, offset)) + } + None => Err(ConvertError::new("Interval", format!("{array:?}")).into()), + } + } ARROW_EXT_TYPE_INTERVAL => { if field.is_nullable() && array.is_null(seq) { return Ok(Value::Null); @@ -914,7 +933,7 @@ fn encode_value(f: &mut std::fmt::Formatter<'_>, val: &Value, raw: bool) -> std: | Value::Bitmap(s) | Value::Variant(s) | Value::Interval(s) - | Value::TimestampTz(s) + | Value::TimestampTzString(s) | Value::Geometry(s) | Value::Geography(s) => { if raw { @@ -992,6 +1011,14 @@ fn encode_value(f: &mut std::fmt::Formatter<'_>, val: &Value, raw: bool) -> std: write!(f, "]")?; Ok(()) } + Value::TimestampTz(ts, offset) => { + let s = timestamp_tz_to_string(*ts, *offset).map_err(|_| std::fmt::Error)?; + if raw { + write!(f, "{s}") + } else { + write!(f, "'{s}'") + } + } } } @@ -1852,7 +1879,7 @@ impl ValueDecoder { fn read_timestamp_tz>(&self, reader: &mut Cursor) -> Result { let mut buf = Vec::new(); reader.read_quoted_text(&mut buf, b'\'')?; - Ok(Value::TimestampTz(unsafe { + Ok(Value::TimestampTzString(unsafe { String::from_utf8_unchecked(buf) })) } @@ -2034,6 +2061,11 @@ impl months_days_micros { } } +#[derive(Debug, Copy, Clone, Default, PartialEq, PartialOrd, Ord, Eq, Hash)] +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct timestamp_tz(pub i128); + // From implementations for basic types to Value impl From<&String> for Value { fn from(s: &String) -> Self { @@ -2233,7 +2265,7 @@ impl Value { let dt = dt.with_timezone(tz); format!("'{}'", dt.format(TIMESTAMP_FORMAT)) } - Value::TimestampTz(t) => format!("'{t}'"), + Value::TimestampTzString(t) => format!("'{t}'"), Value::Date(d) => { let date = NaiveDate::from_num_days_from_ce_opt(*d + DAYS_FROM_CE).unwrap(); format!("'{}'", date.format("%Y-%m-%d")) @@ -2265,10 +2297,21 @@ impl Value { } Value::EmptyArray => "[]".to_string(), Value::EmptyMap => "{}".to_string(), + Value::TimestampTz(ts, offset) => timestamp_tz_to_string(*ts, *offset).unwrap(), } } } +fn timestamp_tz_to_string(ts: i64, offset: i32) -> std::result::Result { + let timestamp = Timestamp::from_microsecond(ts)?; + + let offset = tz::Offset::from_seconds(offset)?; + strtime::format( + TIMESTAMP_TIMEZONE_FORMAT, + ×tamp.to_zoned(offset.to_time_zone()), + ) +} + #[cfg(test)] mod tests { use super::*; From e271dac87993516141b72f28bb7da6d0ced87b97 Mon Sep 17 00:00:00 2001 From: Yang Xiufeng Date: Wed, 12 Nov 2025 17:39:04 +0800 Subject: [PATCH 2/6] feat: return Datetime for TimestampTz in python. --- bindings/nodejs/src/lib.rs | 12 +++- bindings/python/src/types.rs | 1 + .../python/tests/blocking/steps/binding.py | 15 ++++- sql/src/value.rs | 55 +++++++++++-------- 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/bindings/nodejs/src/lib.rs b/bindings/nodejs/src/lib.rs index 999bd7a0..1ccbab1a 100644 --- a/bindings/nodejs/src/lib.rs +++ b/bindings/nodejs/src/lib.rs @@ -25,6 +25,8 @@ use std::sync::Arc; use std::{collections::HashMap, path::Path}; use tokio_stream::StreamExt; +const TIMESTAMP_TIMEZONE_FORMAT: &str = "%Y-%m-%d %H:%M:%S%.6f %z"; + static VERSION: Lazy = Lazy::new(|| { let version = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"); version.to_string() @@ -320,6 +322,13 @@ impl ToNapiValue for Value<'_> { let v = DateTime::::try_from(inner).map_err(format_napi_error)?; DateTime::to_napi_value(env, v) } + databend_driver::Value::TimestampTz(dt) => { + let formatted = dt.format(TIMESTAMP_TIMEZONE_FORMAT); + String::to_napi_value(env, formatted.to_string()) + } + databend_driver::Value::TimestampTzString(s) => { + String::to_napi_value(env, s.to_string()) + } databend_driver::Value::Date(_) => { let inner = val.inner.clone(); let v = NaiveDate::try_from(inner).map_err(format_napi_error)?; @@ -359,9 +368,6 @@ impl ToNapiValue for Value<'_> { String::to_napi_value(env, s.to_string()) } } - databend_driver::Value::TimestampTzString(s) => { - String::to_napi_value(env, s.to_string()) - } databend_driver::Value::Geometry(s) => String::to_napi_value(env, s.to_string()), databend_driver::Value::Interval(s) => String::to_napi_value(env, s.to_string()), databend_driver::Value::Geography(s) => String::to_napi_value(env, s.to_string()), diff --git a/bindings/python/src/types.rs b/bindings/python/src/types.rs index 537d18be..563b2c38 100644 --- a/bindings/python/src/types.rs +++ b/bindings/python/src/types.rs @@ -93,6 +93,7 @@ impl<'py> IntoPyObject<'py> for Value { t.into_bound_py_any(py)? } } + databend_driver::Value::TimestampTz(t) => t.into_bound_py_any(py)?, databend_driver::Value::Date(_) => { let d = NaiveDate::try_from(self.0) .map_err(|e| PyException::new_err(format!("failed to convert date: {e}")))?; diff --git a/bindings/python/tests/blocking/steps/binding.py b/bindings/python/tests/blocking/steps/binding.py index e0f30d8a..1a8300ca 100644 --- a/bindings/python/tests/blocking/steps/binding.py +++ b/bindings/python/tests/blocking/steps/binding.py @@ -144,17 +144,28 @@ def _(context): and DB_VERSION > (1, 2, 836) and sys.version_info.minor >= 8 ): + tz = "Asia/Shanghai" if sys.version_info.minor >= 9: from zoneinfo import ZoneInfo - tz_expected = ZoneInfo("Asia/Shanghai") + tz_expected = ZoneInfo(tz) else: tz_expected = timezone(timedelta(hours=8)) - context.conn.exec("set timezone='Asia/Shanghai'") + context.conn.exec(f"set timezone='{tz}'") row = context.conn.query_row("select to_datetime('2024-04-16 12:34:56.789')") exp = datetime(2024, 4, 16, 12, 34, 56, 789000, tzinfo=tz_expected) assert row.values()[0] == exp, f"Tuple: {row.values()}" + context.conn.exec("set timezone='UTC'") + row = context.conn.query_row(f"settings(timezone='{tz}') select to_datetime('2024-04-16 12:34:56.789')") + exp = datetime(2024, 4, 16, 12, 34, 56, 789000, tzinfo=tz_expected) + assert row.values()[0] == exp, f"Tuple: {row.values()}" + + tz_expected = timezone(timedelta(hours=6)) + row = context.conn.query_row(f"settings(timezone='{tz}') select to_timestamp_tz('2024-04-16 12:34:56.789 +0600')") + exp = datetime(2024, 4, 16, 12, 34, 56, 789000, tzinfo=tz_expected) + exp_bug = datetime(2024, 4, 16, 18, 34, 56, 789000, tzinfo=tz_expected) + assert row.values()[0] in (exp, exp_bug ), f"Tuple: {row.values()[0]} {exp}" @then("Select numbers should iterate all rows") def _(context): diff --git a/sql/src/value.rs b/sql/src/value.rs index a326e68e..2cea482f 100644 --- a/sql/src/value.rs +++ b/sql/src/value.rs @@ -19,14 +19,12 @@ use crate::cursor_ext::{ use crate::error::{ConvertError, Error, Result}; use crate::schema::{DataType, DecimalDataType, DecimalSize, NumberDataType}; use arrow_buffer::i256; -use chrono::{DateTime, Datelike, LocalResult, NaiveDate, NaiveDateTime, TimeZone}; +use chrono::{DateTime, Datelike, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, TimeZone}; use chrono_tz::Tz; use geozero::wkb::FromWkb; use geozero::wkb::WkbDialect; use geozero::wkt::Ewkt; use hex; -use jiff::fmt::strtime; -use jiff::{tz, Timestamp}; use std::collections::HashMap; use std::fmt::{Display, Formatter, Write}; use std::hash::Hash; @@ -87,7 +85,9 @@ pub enum Value { Number(NumberValue), /// Microseconds from 1970-01-01 00:00:00 UTC Timestamp(i64, Tz), - TimestampTz(i64, i32), + // for decode results + TimestampTz(DateTime), + // for encode parameters TimestampTzString(String), Date(i32), Array(Vec), @@ -125,7 +125,7 @@ impl Value { NumberValue::Decimal256(_, s) => DataType::Decimal(DecimalDataType::Decimal256(*s)), }, Self::Timestamp(_, _) => DataType::Timestamp, - Self::TimestampTz(_, _) => DataType::TimestampTz, + Self::TimestampTz(_) => DataType::TimestampTz, Self::TimestampTzString(_) => DataType::TimestampTz, Self::Date(_) => DataType::Date, @@ -238,7 +238,11 @@ impl TryFrom<(&DataType, String, Tz)> for Value { let ts = dt_with_tz.timestamp_micros(); Ok(Self::Timestamp(ts, tz)) } - DataType::TimestampTz => Ok(Self::TimestampTzString(v)), + DataType::TimestampTz => { + let t = + DateTime::::parse_from_str(v.as_str(), TIMESTAMP_TIMEZONE_FORMAT)?; + Ok(Self::TimestampTz(t)) + } DataType::Date => Ok(Self::Date( NaiveDate::parse_from_str(v.as_str(), "%Y-%m-%d")?.num_days_from_ce() - DAYS_FROM_CE, @@ -298,7 +302,21 @@ impl TryFrom<(&ArrowField, &Arc, usize, Tz)> for Value { let v = array.value(seq); let ts = v as u64 as i64; let offset = (v >> 64) as i32; - Ok(Value::TimestampTz(ts, offset)) + + let secs = ts / 1_000_000; + let nanos = ((ts % 1_000_000) * 1000) as u32; + let dt = match DateTime::from_timestamp(secs, nanos) { + Some(t) => { + let off = FixedOffset::east_opt(offset).ok_or_else(|| { + Error::Parsing("invalid offset".to_string()) + })?; + t.with_timezone(&off) + } + None => { + return Err(ConvertError::new("Datetime", format!("{v}")).into()) + } + }; + Ok(Value::TimestampTz(dt)) } None => Err(ConvertError::new("Interval", format!("{array:?}")).into()), } @@ -1011,12 +1029,12 @@ fn encode_value(f: &mut std::fmt::Formatter<'_>, val: &Value, raw: bool) -> std: write!(f, "]")?; Ok(()) } - Value::TimestampTz(ts, offset) => { - let s = timestamp_tz_to_string(*ts, *offset).map_err(|_| std::fmt::Error)?; + Value::TimestampTz(dt) => { + let formatted = dt.format(TIMESTAMP_TIMEZONE_FORMAT); if raw { - write!(f, "{s}") + write!(f, "{formatted}") } else { - write!(f, "'{s}'") + write!(f, "'{formatted}'") } } } @@ -2266,6 +2284,10 @@ impl Value { format!("'{}'", dt.format(TIMESTAMP_FORMAT)) } Value::TimestampTzString(t) => format!("'{t}'"), + Value::TimestampTz(dt) => { + let formatted = dt.format(TIMESTAMP_TIMEZONE_FORMAT); + format!("'{formatted}'") + } Value::Date(d) => { let date = NaiveDate::from_num_days_from_ce_opt(*d + DAYS_FROM_CE).unwrap(); format!("'{}'", date.format("%Y-%m-%d")) @@ -2297,21 +2319,10 @@ impl Value { } Value::EmptyArray => "[]".to_string(), Value::EmptyMap => "{}".to_string(), - Value::TimestampTz(ts, offset) => timestamp_tz_to_string(*ts, *offset).unwrap(), } } } -fn timestamp_tz_to_string(ts: i64, offset: i32) -> std::result::Result { - let timestamp = Timestamp::from_microsecond(ts)?; - - let offset = tz::Offset::from_seconds(offset)?; - strtime::format( - TIMESTAMP_TIMEZONE_FORMAT, - ×tamp.to_zoned(offset.to_time_zone()), - ) -} - #[cfg(test)] mod tests { use super::*; From b091077673a9708c268818b42d4004657c7fda55 Mon Sep 17 00:00:00 2001 From: Yang Xiufeng Date: Wed, 12 Nov 2025 17:42:52 +0800 Subject: [PATCH 3/6] fmt --- bindings/python/tests/blocking/steps/binding.py | 11 ++++++++--- sql/Cargo.toml | 1 - tests/nox/noxfile.py | 9 ++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/bindings/python/tests/blocking/steps/binding.py b/bindings/python/tests/blocking/steps/binding.py index 1a8300ca..4ab5b49d 100644 --- a/bindings/python/tests/blocking/steps/binding.py +++ b/bindings/python/tests/blocking/steps/binding.py @@ -157,15 +157,20 @@ def _(context): assert row.values()[0] == exp, f"Tuple: {row.values()}" context.conn.exec("set timezone='UTC'") - row = context.conn.query_row(f"settings(timezone='{tz}') select to_datetime('2024-04-16 12:34:56.789')") + row = context.conn.query_row( + f"settings(timezone='{tz}') select to_datetime('2024-04-16 12:34:56.789')" + ) exp = datetime(2024, 4, 16, 12, 34, 56, 789000, tzinfo=tz_expected) assert row.values()[0] == exp, f"Tuple: {row.values()}" tz_expected = timezone(timedelta(hours=6)) - row = context.conn.query_row(f"settings(timezone='{tz}') select to_timestamp_tz('2024-04-16 12:34:56.789 +0600')") + row = context.conn.query_row( + f"settings(timezone='{tz}') select to_timestamp_tz('2024-04-16 12:34:56.789 +0600')" + ) exp = datetime(2024, 4, 16, 12, 34, 56, 789000, tzinfo=tz_expected) exp_bug = datetime(2024, 4, 16, 18, 34, 56, 789000, tzinfo=tz_expected) - assert row.values()[0] in (exp, exp_bug ), f"Tuple: {row.values()[0]} {exp}" + assert row.values()[0] in (exp, exp_bug), f"Tuple: {row.values()[0]} {exp}" + @then("Select numbers should iterate all rows") def _(context): diff --git a/sql/Cargo.toml b/sql/Cargo.toml index 0e0bcc9d..6b3936bc 100644 --- a/sql/Cargo.toml +++ b/sql/Cargo.toml @@ -22,7 +22,6 @@ chrono = { workspace = true } chrono-tz = { workspace = true } databend-client = { workspace = true } jsonb = { workspace = true } -jiff = "0.2.10" tokio-stream = { workspace = true } tonic = { workspace = true, optional = true } diff --git a/tests/nox/noxfile.py b/tests/nox/noxfile.py index 60046154..c4f60370 100644 --- a/tests/nox/noxfile.py +++ b/tests/nox/noxfile.py @@ -15,6 +15,7 @@ import nox import os + def generate_params1(): for db_version in ["1.2.803", "1.2.791"]: for body_format in ["arrow", "json"]: @@ -23,6 +24,7 @@ def generate_params1(): continue yield nox.param(db_version, body_format) + @nox.session @nox.parametrize(["db_version", "body_format"], generate_params1()) def new_driver_with_old_servers(session, db_version, body_format): @@ -40,7 +42,7 @@ def new_driver_with_old_servers(session, db_version, body_format): "DATABEND_QUERY_VERSION": query_version, "DATABEND_META_VERSION": query_version, "DB_VERSION": db_version, - "BODY_FORMAT": body_format + "BODY_FORMAT": body_format, } session.run("make", "test-bindings-python", env=env) session.run("make", "down") @@ -61,9 +63,6 @@ def new_test_with_old_drivers(session, driver_version, body_format): session.install("behave") session.install(f"databend-driver=={driver_version}") with session.chdir(".."): - env = { - "DRIVER_VERSION": driver_version, - "BODY_FORMAT": body_format - } + env = {"DRIVER_VERSION": driver_version, "BODY_FORMAT": body_format} session.run("make", "test-bindings-python", env=env) session.run("make", "down") From d46413eeb0281d6d27df7561d7e6c3f5df3660b8 Mon Sep 17 00:00:00 2001 From: Yang Xiufeng Date: Wed, 12 Nov 2025 18:05:16 +0800 Subject: [PATCH 4/6] test TimestampTz in js --- bindings/nodejs/index.d.ts | 2 +- bindings/nodejs/tests/binding.js | 5 +++++ bindings/python/tests/blocking/steps/binding.py | 14 ++++++++------ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/bindings/nodejs/index.d.ts b/bindings/nodejs/index.d.ts index 82d8acb6..7e48d50f 100644 --- a/bindings/nodejs/index.d.ts +++ b/bindings/nodejs/index.d.ts @@ -56,7 +56,7 @@ export declare class Connection { * Load data with stage attachment. * The SQL can be `INSERT INTO tbl VALUES` or `REPLACE INTO tbl VALUES`. */ - streamLoad(sql: string, data: Array>): Promise + streamLoad(sql: string, data: Array>, method?: string | undefined | null): Promise /** * Load file with stage attachment. * The SQL can be `INSERT INTO tbl VALUES` or `REPLACE INTO tbl VALUES`. diff --git a/bindings/nodejs/tests/binding.js b/bindings/nodejs/tests/binding.js index e5e942fd..d6ce771d 100644 --- a/bindings/nodejs/tests/binding.js +++ b/bindings/nodejs/tests/binding.js @@ -216,6 +216,11 @@ Then("Select types should be expected native types", async function () { }, ]); } + // TimestampTz + if (!(DRIVER_VERSION > [0, 30, 3] && DB_VERSION >= [1, 2, 836])) { + const row = await this.conn.queryRow(`SELECT to_datetime_tz('2024-04-16 12:34:56.789 +0800'))`); + assert.deepEqual(row.values(), ["2024-04-16T12:34:56.789 +0800"]); + } }); Then("Select numbers should iterate all rows", async function () { diff --git a/bindings/python/tests/blocking/steps/binding.py b/bindings/python/tests/blocking/steps/binding.py index 4ab5b49d..77cfd6f0 100644 --- a/bindings/python/tests/blocking/steps/binding.py +++ b/bindings/python/tests/blocking/steps/binding.py @@ -155,13 +155,15 @@ def _(context): row = context.conn.query_row("select to_datetime('2024-04-16 12:34:56.789')") exp = datetime(2024, 4, 16, 12, 34, 56, 789000, tzinfo=tz_expected) assert row.values()[0] == exp, f"Tuple: {row.values()}" - context.conn.exec("set timezone='UTC'") - row = context.conn.query_row( - f"settings(timezone='{tz}') select to_datetime('2024-04-16 12:34:56.789')" - ) - exp = datetime(2024, 4, 16, 12, 34, 56, 789000, tzinfo=tz_expected) - assert row.values()[0] == exp, f"Tuple: {row.values()}" + + # wait for release 1.2.839 + # if DB_VERSION >= (1, 2, 839): + # row = context.conn.query_row( + # f"settings(timezone='{tz}') select to_datetime('2024-04-16 12:34:56.789')" + # ) + # exp = datetime(2024, 4, 16, 12, 34, 56, 789000, tzinfo=tz_expected) + # assert row.values()[0] == exp, f"Tuple: {row.values()}" tz_expected = timezone(timedelta(hours=6)) row = context.conn.query_row( From c9504b8ba743ca2c0c963dab2479aa98aac533f9 Mon Sep 17 00:00:00 2001 From: Yang Xiufeng Date: Wed, 12 Nov 2025 18:10:24 +0800 Subject: [PATCH 5/6] fmt --- bindings/nodejs/tests/binding.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/nodejs/tests/binding.js b/bindings/nodejs/tests/binding.js index d6ce771d..449c51a1 100644 --- a/bindings/nodejs/tests/binding.js +++ b/bindings/nodejs/tests/binding.js @@ -218,8 +218,8 @@ Then("Select types should be expected native types", async function () { } // TimestampTz if (!(DRIVER_VERSION > [0, 30, 3] && DB_VERSION >= [1, 2, 836])) { - const row = await this.conn.queryRow(`SELECT to_datetime_tz('2024-04-16 12:34:56.789 +0800'))`); - assert.deepEqual(row.values(), ["2024-04-16T12:34:56.789 +0800"]); + const row = await this.conn.queryRow(`SELECT to_datetime_tz('2024-04-16 12:34:56.789 +0800'))`); + assert.deepEqual(row.values(), ["2024-04-16T12:34:56.789 +0800"]); } }); From 3fa0a2a0cbf639049dc763bc10e866dbde3b6655 Mon Sep 17 00:00:00 2001 From: Yang Xiufeng Date: Thu, 13 Nov 2025 10:18:07 +0800 Subject: [PATCH 6/6] rm Value::TimestampTzString --- bindings/nodejs/src/lib.rs | 3 --- bindings/python/src/types.rs | 1 - sql/src/value.rs | 12 +++--------- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/bindings/nodejs/src/lib.rs b/bindings/nodejs/src/lib.rs index 1ccbab1a..ac12c478 100644 --- a/bindings/nodejs/src/lib.rs +++ b/bindings/nodejs/src/lib.rs @@ -326,9 +326,6 @@ impl ToNapiValue for Value<'_> { let formatted = dt.format(TIMESTAMP_TIMEZONE_FORMAT); String::to_napi_value(env, formatted.to_string()) } - databend_driver::Value::TimestampTzString(s) => { - String::to_napi_value(env, s.to_string()) - } databend_driver::Value::Date(_) => { let inner = val.inner.clone(); let v = NaiveDate::try_from(inner).map_err(format_napi_error)?; diff --git a/bindings/python/src/types.rs b/bindings/python/src/types.rs index 563b2c38..a438084a 100644 --- a/bindings/python/src/types.rs +++ b/bindings/python/src/types.rs @@ -114,7 +114,6 @@ impl<'py> IntoPyObject<'py> for Value { let tuple = PyTuple::new(py, inner.into_iter().map(Value))?; tuple.into_bound_py_any(py)? } - databend_driver::Value::TimestampTzString(s) => s.into_bound_py_any(py)?, databend_driver::Value::Bitmap(s) => s.into_bound_py_any(py)?, databend_driver::Value::Variant(s) => s.into_bound_py_any(py)?, databend_driver::Value::Geometry(s) => s.into_bound_py_any(py)?, diff --git a/sql/src/value.rs b/sql/src/value.rs index 2cea482f..af2d4bcc 100644 --- a/sql/src/value.rs +++ b/sql/src/value.rs @@ -85,10 +85,7 @@ pub enum Value { Number(NumberValue), /// Microseconds from 1970-01-01 00:00:00 UTC Timestamp(i64, Tz), - // for decode results TimestampTz(DateTime), - // for encode parameters - TimestampTzString(String), Date(i32), Array(Vec), Map(Vec<(Value, Value)>), @@ -126,7 +123,6 @@ impl Value { }, Self::Timestamp(_, _) => DataType::Timestamp, Self::TimestampTz(_) => DataType::TimestampTz, - Self::TimestampTzString(_) => DataType::TimestampTz, Self::Date(_) => DataType::Date, Self::Interval(_) => DataType::Interval, @@ -951,7 +947,6 @@ fn encode_value(f: &mut std::fmt::Formatter<'_>, val: &Value, raw: bool) -> std: | Value::Bitmap(s) | Value::Variant(s) | Value::Interval(s) - | Value::TimestampTzString(s) | Value::Geometry(s) | Value::Geography(s) => { if raw { @@ -1897,9 +1892,9 @@ impl ValueDecoder { fn read_timestamp_tz>(&self, reader: &mut Cursor) -> Result { let mut buf = Vec::new(); reader.read_quoted_text(&mut buf, b'\'')?; - Ok(Value::TimestampTzString(unsafe { - String::from_utf8_unchecked(buf) - })) + let v = unsafe { std::str::from_utf8_unchecked(&buf) }; + let t = DateTime::::parse_from_str(v, TIMESTAMP_TIMEZONE_FORMAT)?; + Ok(Value::TimestampTz(t)) } fn read_bitmap>(&self, reader: &mut Cursor) -> Result { @@ -2283,7 +2278,6 @@ impl Value { let dt = dt.with_timezone(tz); format!("'{}'", dt.format(TIMESTAMP_FORMAT)) } - Value::TimestampTzString(t) => format!("'{t}'"), Value::TimestampTz(dt) => { let formatted = dt.format(TIMESTAMP_TIMEZONE_FORMAT); format!("'{formatted}'")