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:
Ning Sun
2023-05-22 18:30:23 +08:00
committed by GitHub
parent 32ad358323
commit 067c5ee7ce
11 changed files with 380 additions and 46 deletions

View File

@@ -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"

View File

@@ -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),
}
}

View File

@@ -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;

View File

@@ -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())
);
}
}

View 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()
);
}
}