mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-09-06 13:38:59 +00:00
fix(mysql): interpret prepared statement datetime params in session timezone (#8923)
* fix(mysql): interpret prepared statement datetime params in session timezone Binary DATETIME parameters of server-side prepared statements were converted as if UTC, ignoring the session timezone set via SET time_zone. Convert them with the session timezone and add an integration test covering prepared inserts and predicates under Asia/Shanghai. Signed-off-by: wy471x <wy471x@gmail.com> * refactor: share naive datetime timezone policy via common-time Address review feedback on the prepared-statement timezone fix: - Expose Timestamp::from_naive_datetime in common-time so the DST policy (gap -> error, ambiguous -> earlier instant) lives in one place, shared by the text protocol (Timestamp::from_str) and the MySQL binary protocol. - Route the MySQL prepared-statement datetime conversion through it. - Match the target type before converting datetime params so PreparedStmtTypeMismatch fails fast without wasted conversion. - Use the short Timezone import form for consistency with the rest of servers. Signed-off-by: wy471x <wy471x@gmail.com> --------- Signed-off-by: wy471x <wy471x@gmail.com> Co-authored-by: Ning Sun <sunng@protonmail.com>
This commit is contained in:
@@ -483,6 +483,31 @@ impl Timestamp {
|
||||
ParseTimestampSnafu { raw: s }.fail()
|
||||
}
|
||||
|
||||
/// Interprets a timezone-less [`NaiveDateTime`] in the given timezone and
|
||||
/// returns the corresponding timestamp.
|
||||
///
|
||||
/// Datetimes that fall into a DST gap (a local time that does not exist)
|
||||
/// are rejected, while ambiguous datetimes (from a repeated local time)
|
||||
/// are resolved to the earlier instant. This policy is shared with
|
||||
/// [`Timestamp::from_str`] so that the text and binary protocols interpret
|
||||
/// datetimes consistently.
|
||||
pub fn from_naive_datetime(
|
||||
datetime: NaiveDateTime,
|
||||
timezone: &Timezone,
|
||||
) -> crate::error::Result<Timestamp> {
|
||||
match datetime_to_utc(&datetime, timezone) {
|
||||
LocalResult::Single(utc) | LocalResult::Ambiguous(utc, _) => {
|
||||
Timestamp::from_chrono_datetime(utc).context(ParseTimestampSnafu {
|
||||
raw: format!("{datetime} (timezone {timezone})"),
|
||||
})
|
||||
}
|
||||
LocalResult::None => ParseTimestampSnafu {
|
||||
raw: format!("{datetime} (timezone {timezone})"),
|
||||
}
|
||||
.fail(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn negative(mut self) -> Self {
|
||||
self.value = -self.value;
|
||||
self
|
||||
@@ -531,12 +556,8 @@ fn naive_datetime_to_timestamp(
|
||||
.context(ParseTimestampSnafu { raw: s });
|
||||
};
|
||||
|
||||
match datetime_to_utc(&datetime, timezone) {
|
||||
LocalResult::None => ParseTimestampSnafu { raw: s }.fail(),
|
||||
LocalResult::Single(utc) | LocalResult::Ambiguous(utc, _) => {
|
||||
Timestamp::from_chrono_datetime(utc).context(ParseTimestampSnafu { raw: s })
|
||||
}
|
||||
}
|
||||
Timestamp::from_naive_datetime(datetime, timezone)
|
||||
.map_err(|_| ParseTimestampSnafu { raw: s }.build())
|
||||
}
|
||||
|
||||
impl From<i64> for Timestamp {
|
||||
@@ -919,6 +940,48 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_naive_datetime() {
|
||||
let datetime = NaiveDate::from_ymd_opt(2026, 8, 13)
|
||||
.unwrap()
|
||||
.and_hms_opt(8, 0, 0)
|
||||
.unwrap();
|
||||
|
||||
// A fixed-offset timezone shifts the datetime by a constant amount.
|
||||
let shanghai = Timezone::from_tz_string("Asia/Shanghai").unwrap();
|
||||
assert_eq!(
|
||||
"2026-08-13 00:00:00",
|
||||
Timestamp::from_naive_datetime(datetime, &shanghai)
|
||||
.unwrap()
|
||||
.to_chrono_datetime()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
);
|
||||
|
||||
// 2026-03-08 02:30 does not exist in America/New_York (DST gap).
|
||||
let new_york = Timezone::from_tz_string("America/New_York").unwrap();
|
||||
let gap = NaiveDate::from_ymd_opt(2026, 3, 8)
|
||||
.unwrap()
|
||||
.and_hms_opt(2, 30, 0)
|
||||
.unwrap();
|
||||
assert!(Timestamp::from_naive_datetime(gap, &new_york).is_err());
|
||||
|
||||
// 2026-11-01 01:30 is ambiguous in America/New_York; picks the first
|
||||
// instant (EDT, UTC-4).
|
||||
let ambiguous = NaiveDate::from_ymd_opt(2026, 11, 1)
|
||||
.unwrap()
|
||||
.and_hms_opt(1, 30, 0)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
"2026-11-01 05:30:00",
|
||||
Timestamp::from_naive_datetime(ambiguous, &new_york)
|
||||
.unwrap()
|
||||
.to_chrono_datetime()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_iso8601_string() {
|
||||
set_default_timezone(Some("Asia/Shanghai")).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user