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:
wy471x
2026-08-28 02:40:25 +00:00
committed by GitHub
co-authored by Ning Sun
parent a2f39ecf7b
commit aaa843104b
4 changed files with 286 additions and 44 deletions
+69 -6
View File
@@ -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();