mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-05-26 01:40:36 +00:00
feat: time_zone variable for mysql connections (#1607)
* feat: add timezone info to query context * feat: parse mysql compatible time zone string * feat: add method to timestamp for rendering timezone aware string * feat: use timezone from session for time string rendering * refactor: use querycontectref * feat: implement session/timezone variable read/write * style: resolve toml format * test: update tests * Apply suggestions from code review Co-authored-by: dennis zhuang <killme2008@gmail.com> * Update src/session/src/context.rs Co-authored-by: dennis zhuang <killme2008@gmail.com> * refactor: address review issues --------- Co-authored-by: dennis zhuang <killme2008@gmail.com>
This commit is contained in:
@@ -6,6 +6,7 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
chrono.workspace = true
|
||||
chrono-tz = "0.8"
|
||||
common-error = { path = "../error" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::any::Any;
|
||||
use std::num::TryFromIntError;
|
||||
use std::num::{ParseIntError, TryFromIntError};
|
||||
|
||||
use chrono::ParseError;
|
||||
use common_error::ext::ErrorExt;
|
||||
@@ -40,14 +40,33 @@ pub enum Error {
|
||||
|
||||
#[snafu(display("Timestamp arithmetic overflow, msg: {}", msg))]
|
||||
ArithmeticOverflow { msg: String, location: Location },
|
||||
|
||||
#[snafu(display("Invalid time zone offset: {hours}:{minutes}"))]
|
||||
InvalidTimeZoneOffset {
|
||||
hours: i32,
|
||||
minutes: u32,
|
||||
location: Location,
|
||||
},
|
||||
|
||||
#[snafu(display("Invalid offset string {raw}: {source}"))]
|
||||
ParseOffsetStr {
|
||||
raw: String,
|
||||
source: ParseIntError,
|
||||
location: Location,
|
||||
},
|
||||
|
||||
#[snafu(display("Invalid time zone string {raw}"))]
|
||||
ParseTimeZoneName { raw: String, location: Location },
|
||||
}
|
||||
|
||||
impl ErrorExt for Error {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Error::ParseDateStr { .. } | Error::ParseTimestamp { .. } => {
|
||||
StatusCode::InvalidArguments
|
||||
}
|
||||
Error::ParseDateStr { .. }
|
||||
| Error::ParseTimestamp { .. }
|
||||
| Error::InvalidTimeZoneOffset { .. }
|
||||
| Error::ParseOffsetStr { .. }
|
||||
| Error::ParseTimeZoneName { .. } => StatusCode::InvalidArguments,
|
||||
Error::TimestampOverflow { .. } => StatusCode::Internal,
|
||||
Error::InvalidDateStr { .. } | Error::ArithmeticOverflow { .. } => {
|
||||
StatusCode::InvalidArguments
|
||||
@@ -64,7 +83,10 @@ impl ErrorExt for Error {
|
||||
Error::ParseTimestamp { location, .. }
|
||||
| Error::TimestampOverflow { location, .. }
|
||||
| Error::ArithmeticOverflow { location, .. } => Some(*location),
|
||||
Error::ParseDateStr { .. } => None,
|
||||
Error::ParseDateStr { .. }
|
||||
| Error::InvalidTimeZoneOffset { .. }
|
||||
| Error::ParseOffsetStr { .. }
|
||||
| Error::ParseTimeZoneName { .. } => None,
|
||||
Error::InvalidDateStr { location, .. } => Some(*location),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ pub mod error;
|
||||
pub mod range;
|
||||
pub mod timestamp;
|
||||
pub mod timestamp_millis;
|
||||
pub mod timezone;
|
||||
pub mod util;
|
||||
|
||||
pub use date::Date;
|
||||
@@ -25,3 +26,4 @@ pub use datetime::DateTime;
|
||||
pub use range::RangeMillis;
|
||||
pub use timestamp::Timestamp;
|
||||
pub use timestamp_millis::TimestampMillis;
|
||||
pub use timezone::TimeZone;
|
||||
|
||||
@@ -20,12 +20,13 @@ use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::offset::Local;
|
||||
use chrono::{DateTime, LocalResult, NaiveDateTime, TimeZone, Utc};
|
||||
use chrono::{DateTime, LocalResult, NaiveDateTime, TimeZone as ChronoTimeZone, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use snafu::{OptionExt, ResultExt};
|
||||
|
||||
use crate::error;
|
||||
use crate::error::{ArithmeticOverflowSnafu, Error, ParseTimestampSnafu, TimestampOverflowSnafu};
|
||||
use crate::timezone::TimeZone;
|
||||
use crate::util::div_ceil;
|
||||
|
||||
#[derive(Debug, Clone, Default, Copy, Serialize, Deserialize)]
|
||||
@@ -171,17 +172,33 @@ impl Timestamp {
|
||||
/// Format timestamp to ISO8601 string. If the timestamp exceeds what chrono timestamp can
|
||||
/// represent, this function simply print the timestamp unit and value in plain string.
|
||||
pub fn to_iso8601_string(&self) -> String {
|
||||
self.as_formatted_string("%Y-%m-%d %H:%M:%S%.f%z")
|
||||
self.as_formatted_string("%Y-%m-%d %H:%M:%S%.f%z", None)
|
||||
}
|
||||
|
||||
pub fn to_local_string(&self) -> String {
|
||||
self.as_formatted_string("%Y-%m-%d %H:%M:%S%.f")
|
||||
self.as_formatted_string("%Y-%m-%d %H:%M:%S%.f", None)
|
||||
}
|
||||
|
||||
fn as_formatted_string(self, pattern: &str) -> String {
|
||||
/// Format timestamp for given timezone.
|
||||
/// When timezone is None, using local time by default.
|
||||
pub fn to_timezone_aware_string(&self, tz: Option<TimeZone>) -> String {
|
||||
self.as_formatted_string("%Y-%m-%d %H:%M:%S%.f", tz)
|
||||
}
|
||||
|
||||
fn as_formatted_string(self, pattern: &str, timezone: Option<TimeZone>) -> String {
|
||||
if let Some(v) = self.to_chrono_datetime() {
|
||||
let local = Local {};
|
||||
format!("{}", local.from_utc_datetime(&v).format(pattern))
|
||||
match timezone {
|
||||
Some(TimeZone::Offset(offset)) => {
|
||||
format!("{}", offset.from_utc_datetime(&v).format(pattern))
|
||||
}
|
||||
Some(TimeZone::Named(tz)) => {
|
||||
format!("{}", tz.from_utc_datetime(&v).format(pattern))
|
||||
}
|
||||
None => {
|
||||
let local = Local {};
|
||||
format!("{}", local.from_utc_datetime(&v).format(pattern))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
format!("[Timestamp{}: {}]", self.unit, self.value)
|
||||
}
|
||||
@@ -934,4 +951,54 @@ mod tests {
|
||||
Timestamp::new_millisecond(58).sub(Timestamp::new_millisecond(100))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_timezone_aware_string() {
|
||||
std::env::set_var("TZ", "Asia/Shanghai");
|
||||
|
||||
assert_eq!(
|
||||
"1970-01-01 08:00:00.001",
|
||||
Timestamp::new(1, TimeUnit::Millisecond).to_timezone_aware_string(None)
|
||||
);
|
||||
assert_eq!(
|
||||
"1970-01-01 08:00:00.001",
|
||||
Timestamp::new(1, TimeUnit::Millisecond)
|
||||
.to_timezone_aware_string(TimeZone::from_tz_string("SYSTEM").unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
"1970-01-01 08:00:00.001",
|
||||
Timestamp::new(1, TimeUnit::Millisecond)
|
||||
.to_timezone_aware_string(TimeZone::from_tz_string("+08:00").unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
"1970-01-01 07:00:00.001",
|
||||
Timestamp::new(1, TimeUnit::Millisecond)
|
||||
.to_timezone_aware_string(TimeZone::from_tz_string("+07:00").unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
"1969-12-31 23:00:00.001",
|
||||
Timestamp::new(1, TimeUnit::Millisecond)
|
||||
.to_timezone_aware_string(TimeZone::from_tz_string("-01:00").unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
"1970-01-01 08:00:00.001",
|
||||
Timestamp::new(1, TimeUnit::Millisecond)
|
||||
.to_timezone_aware_string(TimeZone::from_tz_string("Asia/Shanghai").unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
"1970-01-01 00:00:00.001",
|
||||
Timestamp::new(1, TimeUnit::Millisecond)
|
||||
.to_timezone_aware_string(TimeZone::from_tz_string("UTC").unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
"1970-01-01 01:00:00.001",
|
||||
Timestamp::new(1, TimeUnit::Millisecond)
|
||||
.to_timezone_aware_string(TimeZone::from_tz_string("Europe/Berlin").unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
"1970-01-01 03:00:00.001",
|
||||
Timestamp::new(1, TimeUnit::Millisecond)
|
||||
.to_timezone_aware_string(TimeZone::from_tz_string("Europe/Moscow").unwrap())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
158
src/common/time/src/timezone.rs
Normal file
158
src/common/time/src/timezone.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
// Copyright 2023 Greptime Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::{FixedOffset, Local};
|
||||
use chrono_tz::Tz;
|
||||
use snafu::{OptionExt, ResultExt};
|
||||
|
||||
use crate::error::{
|
||||
InvalidTimeZoneOffsetSnafu, ParseOffsetStrSnafu, ParseTimeZoneNameSnafu, Result,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TimeZone {
|
||||
Offset(FixedOffset),
|
||||
Named(Tz),
|
||||
}
|
||||
|
||||
impl TimeZone {
|
||||
/// Compute timezone from given offset hours and minutes
|
||||
/// Return `None` if given offset exceeds scope
|
||||
pub fn hours_mins_opt(offset_hours: i32, offset_mins: u32) -> Result<Self> {
|
||||
let offset_secs = if offset_hours > 0 {
|
||||
offset_hours * 3600 + offset_mins as i32 * 60
|
||||
} else {
|
||||
offset_hours * 3600 - offset_mins as i32 * 60
|
||||
};
|
||||
|
||||
FixedOffset::east_opt(offset_secs)
|
||||
.map(Self::Offset)
|
||||
.context(InvalidTimeZoneOffsetSnafu {
|
||||
hours: offset_hours,
|
||||
minutes: offset_mins,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse timezone offset string and return None if given offset exceeds
|
||||
/// scope.
|
||||
///
|
||||
/// String examples are available as described in
|
||||
/// https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html
|
||||
///
|
||||
/// - `SYSTEM`
|
||||
/// - Offset to UTC: `+08:00` , `-11:30`
|
||||
/// - Named zones: `Asia/Shanghai`, `Europe/Berlin`
|
||||
pub fn from_tz_string(tz_string: &str) -> Result<Option<Self>> {
|
||||
// Use system timezone
|
||||
if tz_string.eq_ignore_ascii_case("SYSTEM") {
|
||||
Ok(None)
|
||||
} else if let Some((hrs, mins)) = tz_string.split_once(':') {
|
||||
let hrs = hrs
|
||||
.parse::<i32>()
|
||||
.context(ParseOffsetStrSnafu { raw: tz_string })?;
|
||||
let mins = mins
|
||||
.parse::<u32>()
|
||||
.context(ParseOffsetStrSnafu { raw: tz_string })?;
|
||||
Self::hours_mins_opt(hrs, mins).map(Some)
|
||||
} else if let Ok(tz) = Tz::from_str(tz_string) {
|
||||
Ok(Some(Self::Named(tz)))
|
||||
} else {
|
||||
ParseTimeZoneNameSnafu { raw: tz_string }.fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TimeZone {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Named(tz) => write!(f, "{}", tz.name()),
|
||||
Self::Offset(offset) => write!(f, "{}", offset),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn system_time_zone_name() -> String {
|
||||
Local::now().offset().to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_from_tz_string() {
|
||||
assert_eq!(None, TimeZone::from_tz_string("SYSTEM").unwrap());
|
||||
|
||||
let utc_plus_8 = Some(TimeZone::Offset(FixedOffset::east_opt(3600 * 8).unwrap()));
|
||||
assert_eq!(utc_plus_8, TimeZone::from_tz_string("+8:00").unwrap());
|
||||
assert_eq!(utc_plus_8, TimeZone::from_tz_string("+08:00").unwrap());
|
||||
assert_eq!(utc_plus_8, TimeZone::from_tz_string("08:00").unwrap());
|
||||
|
||||
let utc_minus_8 = Some(TimeZone::Offset(FixedOffset::west_opt(3600 * 8).unwrap()));
|
||||
assert_eq!(utc_minus_8, TimeZone::from_tz_string("-08:00").unwrap());
|
||||
assert_eq!(utc_minus_8, TimeZone::from_tz_string("-8:00").unwrap());
|
||||
|
||||
let utc_minus_8_5 = Some(TimeZone::Offset(
|
||||
FixedOffset::west_opt(3600 * 8 + 60 * 30).unwrap(),
|
||||
));
|
||||
assert_eq!(utc_minus_8_5, TimeZone::from_tz_string("-8:30").unwrap());
|
||||
|
||||
let utc_plus_max = Some(TimeZone::Offset(FixedOffset::east_opt(3600 * 14).unwrap()));
|
||||
assert_eq!(utc_plus_max, TimeZone::from_tz_string("14:00").unwrap());
|
||||
|
||||
let utc_minus_max = Some(TimeZone::Offset(
|
||||
FixedOffset::west_opt(3600 * 13 + 60 * 59).unwrap(),
|
||||
));
|
||||
assert_eq!(utc_minus_max, TimeZone::from_tz_string("-13:59").unwrap());
|
||||
|
||||
assert_eq!(
|
||||
Some(TimeZone::Named(Tz::Asia__Shanghai)),
|
||||
TimeZone::from_tz_string("Asia/Shanghai").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
Some(TimeZone::Named(Tz::UTC)),
|
||||
TimeZone::from_tz_string("UTC").unwrap()
|
||||
);
|
||||
|
||||
assert!(TimeZone::from_tz_string("WORLD_PEACE").is_err());
|
||||
assert!(TimeZone::from_tz_string("A0:01").is_err());
|
||||
assert!(TimeZone::from_tz_string("20:0A").is_err());
|
||||
assert!(TimeZone::from_tz_string(":::::").is_err());
|
||||
assert!(TimeZone::from_tz_string("Asia/London").is_err());
|
||||
assert!(TimeZone::from_tz_string("Unknown").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timezone_to_string() {
|
||||
assert_eq!("UTC", TimeZone::Named(Tz::UTC).to_string());
|
||||
assert_eq!(
|
||||
"+01:00",
|
||||
TimeZone::from_tz_string("01:00")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
"Asia/Shanghai",
|
||||
TimeZone::from_tz_string("Asia/Shanghai")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user