Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ members = [
"src/common/tracing",
"src/common/storage",
"src/common/vector",
"src/common/timezone",
"src/common/license",
"src/common/version",
"src/query/ast",
Expand Down Expand Up @@ -175,6 +176,7 @@ databend-common-storages-stage = { path = "src/query/storages/stage" }
databend-common-storages-stream = { path = "src/query/storages/stream" }
databend-common-storages-system = { path = "src/query/storages/system" }
databend-common-telemetry = { path = "src/common/telemetry" }
databend-common-timezone = { path = "src/common/timezone" }
databend-common-tracing = { path = "src/common/tracing" }
databend-common-users = { path = "src/query/users" }
databend-common-vector = { path = "src/common/vector" }
Expand Down Expand Up @@ -352,7 +354,7 @@ jaq-core = "1.5.1"
jaq-interpret = "1.5.0"
jaq-parse = "1.0.3"
jaq-std = "1.6.0"
jiff = { version = "0.2.10", features = ["serde", "tzdb-bundle-always"] }
jiff = { version = "0.2.16", features = ["serde", "tzdb-bundle-always"] }
jsonb = "0.5.5"
jwt-simple = { version = "0.12.10", default-features = false, features = ["pure-rust"] }
lenient_semver = "0.4.2"
Expand Down
1 change: 1 addition & 0 deletions src/common/io/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ chrono = { workspace = true }
chrono-tz = { workspace = true }
databend-common-base = { workspace = true }
databend-common-exception = { workspace = true }
databend-common-timezone = { workspace = true }
enquote = { workspace = true }
enumflags2 = { workspace = true }
ethnum = { workspace = true }
Expand Down
105 changes: 50 additions & 55 deletions src/common/io/src/cursor_ext/cursor_read_datetime_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use chrono_tz::Tz;
use databend_common_exception::ErrorCode;
use databend_common_exception::Result;
use databend_common_exception::ToErrorCode;
use jiff::civil::date;
use databend_common_timezone::fast_utc_from_local;
use jiff::civil::Date;
use jiff::tz::Offset;
use jiff::tz::TimeZone;
Expand All @@ -45,7 +45,6 @@ pub trait BufferReadDateTimeExt {
fn read_timestamp_text(&mut self, tz: &TimeZone) -> Result<DateTimeResType>;
fn parse_time_offset(
&mut self,
tz: &TimeZone,
buf: &mut Vec<u8>,
dt: &Zoned,
west_tz: bool,
Expand Down Expand Up @@ -87,14 +86,12 @@ where T: AsRef<[u8]>
// Only support HH:mm format
fn parse_time_offset(
&mut self,
tz: &TimeZone,
buf: &mut Vec<u8>,
dt: &Zoned,
west_tz: bool,
calc_offset: impl Fn(i64, i64, &Zoned) -> Result<Zoned>,
) -> Result<Zoned> {
fn get_hour_minute_offset(
tz: &TimeZone,
dt: &Zoned,
west_tz: bool,
calc_offset: &impl Fn(i64, i64, &Zoned) -> Result<Zoned>,
Expand All @@ -104,24 +101,14 @@ where T: AsRef<[u8]>
if (hour_offset == 14 && minute_offset == 0)
|| ((0..60).contains(&minute_offset) && hour_offset < 14)
{
if dt.year() < 1970 {
Ok(date(1970, 1, 1)
.at(0, 0, 0, 0)
.to_zoned(tz.clone())
.map_err_to_code(ErrorCode::BadBytes, || format!("dt parse error"))?)
} else {
let current_tz_sec = dt.offset().seconds();
let mut val_tz_sec =
Offset::from_seconds(hour_offset * 3600 + minute_offset * 60)
.map_err_to_code(ErrorCode::BadBytes, || {
"calc offset failed.".to_string()
})?
.seconds();
if west_tz {
val_tz_sec = -val_tz_sec;
}
calc_offset(current_tz_sec.into(), val_tz_sec.into(), dt)
let current_tz_sec = dt.offset().seconds();
let mut val_tz_sec = Offset::from_seconds(hour_offset * 3600 + minute_offset * 60)
.map_err_to_code(ErrorCode::BadBytes, || "calc offset failed.".to_string())?
.seconds();
if west_tz {
val_tz_sec = -val_tz_sec;
}
calc_offset(current_tz_sec.into(), val_tz_sec.into(), dt)
} else {
Err(ErrorCode::BadBytes(format!(
"Invalid Timezone Offset: The minute offset '{}' is outside the valid range. Expected range is [00-59] within a timezone gap of [-14:00, +14:00]",
Expand All @@ -146,16 +133,9 @@ where T: AsRef<[u8]>
let minute_offset: i32 =
lexical_core::FromLexical::from_lexical(buf.as_slice()).map_err_to_code(ErrorCode::BadBytes, || "minute offset parse error".to_string())?;
// max utc: 14:00, min utc: 00:00
get_hour_minute_offset(
tz,
dt,
west_tz,
&calc_offset,
hour_offset,
minute_offset,
)
get_hour_minute_offset(dt, west_tz, &calc_offset, hour_offset, minute_offset)
} else {
get_hour_minute_offset(tz, dt, west_tz, &calc_offset, hour_offset, 0)
get_hour_minute_offset(dt, west_tz, &calc_offset, hour_offset, 0)
}
} else {
Err(ErrorCode::BadBytes(format!(
Expand All @@ -174,14 +154,7 @@ where T: AsRef<[u8]>
buf.clear();
// max utc: 14:00, min utc: 00:00
if (0..15).contains(&hour_offset) {
get_hour_minute_offset(
tz,
dt,
west_tz,
&calc_offset,
hour_offset,
minute_offset,
)
get_hour_minute_offset(dt, west_tz, &calc_offset, hour_offset, minute_offset)
} else {
Err(ErrorCode::BadBytes(format!(
"Invalid Timezone Offset: The hour offset '{}' is outside the valid range. Expected range is [00-14] within a timezone gap of [-14:00, +14:00]",
Expand Down Expand Up @@ -279,13 +252,9 @@ where T: AsRef<[u8]>
buf.clear();
let calc_offset = |current_tz_sec: i64, val_tz_sec: i64, dt: &Zoned| {
let offset = (current_tz_sec - val_tz_sec) * 1000 * 1000;
let mut ts = dt.timestamp().as_microsecond();
ts += offset;
let (mut secs, mut micros) = (ts / 1_000_000, ts % 1_000_000);
if ts < 0 {
secs -= 1;
micros += 1_000_000;
}
let ts = dt.timestamp().as_microsecond() + offset;
let secs = ts.div_euclid(1_000_000);
let micros = ts.rem_euclid(1_000_000);
Ok(Timestamp::new(secs, (micros as i32) * 1000)
.map_err_to_code(ErrorCode::BadBytes, || {
format!("Datetime {} add offset {} with error", dt, offset)
Expand All @@ -302,15 +271,13 @@ where T: AsRef<[u8]>
)?))
} else if self.ignore_byte(b'+') {
Ok(DateTimeResType::Datetime(self.parse_time_offset(
tz,
&mut buf,
&dt,
false,
calc_offset,
)?))
} else if self.ignore_byte(b'-') {
Ok(DateTimeResType::Datetime(self.parse_time_offset(
tz,
&mut buf,
&dt,
true,
Expand All @@ -324,6 +291,8 @@ where T: AsRef<[u8]>
// only date part
if need_date {
Ok(DateTimeResType::Date(d))
} else if let Some(zoned) = fast_local_to_zoned(tz, &d, 0, 0, 0, 0) {
Ok(DateTimeResType::Datetime(zoned))
} else {
Ok(DateTimeResType::Datetime(
d.to_zoned(tz.clone())
Expand All @@ -336,15 +305,41 @@ where T: AsRef<[u8]>
}
}

// Can not directly unwrap, because of DST.
// e.g.
// set timezone='Europe/London';
// -- if unwrap() will cause session panic.
// -- https://github.com/chronotope/chrono/blob/v0.4.24/src/offset/mod.rs#L186
// select to_date(to_timestamp('2021-03-28 01:00:00'));
// Now add a setting enable_dst_hour_fix to control this behavior. If true, try to add a hour.
/// Convert a local civil time into a `Zoned` instant by first attempting the
/// LUT-based `fast_utc_from_local`. When the LUT cannot represent the request
/// (e.g. outside 1900–2299 or in a DST gap), fall back to Jiff's slower but
/// fully general conversion. The behavior mirrors ClickHouse/Jiff: gaps return
/// `None`, folds prefer the later instant.
fn fast_local_to_zoned(
tz: &TimeZone,
date: &Date,
hour: u8,
minute: u8,
second: u8,
micro: u32,
) -> Option<Zoned> {
let micros = fast_utc_from_local(
tz,
i32::from(date.year()),
date.month() as u8,
date.day() as u8,
hour,
minute,
second,
micro,
)?;
let ts = Timestamp::from_microsecond(micros).ok()?;
Some(ts.to_zoned(tz.clone()))
}

fn get_local_time(tz: &TimeZone, d: &Date, times: &mut Vec<u32>) -> Result<Zoned> {
d.at(times[0] as i8, times[1] as i8, times[2] as i8, 0)
let hour = times[0] as u8;
let minute = times[1] as u8;
let second = times[2] as u8;
if let Some(zoned) = fast_local_to_zoned(tz, d, hour, minute, second, 0) {
return Ok(zoned);
}
d.at(hour as i8, minute as i8, second as i8, 0)
.to_zoned(tz.clone())
.map_err_to_code(ErrorCode::BadBytes, || {
format!("Invalid time provided in times: {:?}", times)
Expand Down
4 changes: 2 additions & 2 deletions src/common/io/tests/it/cursor_ext/read_datetime_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ fn test_read_timestamp_text() -> Result<()> {
"2020-01-01T11:11:11.123+00:00[UTC]",
"2055-02-03T02:00:20.234+00:00[UTC]",
"2055-02-03T18:00:20.234+00:00[UTC]",
"1970-01-01T00:00:00+00:00[UTC]",
"1022-05-15T19:25:02+00:00[UTC]",
];
let mut res = vec![];
for _ in 0..expected.len() {
Expand Down Expand Up @@ -123,7 +123,7 @@ fn test_read_date_text() -> Result<()> {
"2020-01-01",
"2055-02-03",
"2055-02-03",
"1970-01-01",
"1022-05-15",
"2055-01-01",
];

Expand Down
16 changes: 16 additions & 0 deletions src/common/timezone/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "databend-common-timezone"
version = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
publish = { workspace = true }
edition = { workspace = true }

[dependencies]
jiff = { workspace = true }

[dev-dependencies]
rand = { workspace = true }

[lints]
workspace = true
Loading
Loading