@@ -101,6 +101,12 @@ pub fn string_to_timestamp_nanos(s: &str) -> Result<i64> {
101101 return Ok ( ts) ;
102102 }
103103
104+ // Try to parse a trailing IANA timezone name, e.g.
105+ // `2026-06-15 00:00:00 America/Los_Angeles`, which PostgreSQL accepts.
106+ if let Some ( ts) = parse_timestamp_with_named_tz ( s) {
107+ return Ok ( ts) ;
108+ }
109+
104110 // Support timestamps without an explicit timezone offset, again
105111 // to be compatible with what Apache Spark SQL does.
106112
@@ -223,6 +229,38 @@ fn parse_timestamp_with_manual_offset(s: &str) -> Option<i64> {
223229 None
224230}
225231
232+ /// Try to parse a timestamp with a trailing IANA timezone name, such as
233+ /// `2026-06-15 00:00:00 America/Los_Angeles`. The timezone name is the
234+ /// whitespace-separated token after the time component and is resolved
235+ /// against `chrono-tz`. The wall-clock time is interpreted in that
236+ /// timezone and converted to a UTC nanosecond timestamp.
237+ ///
238+ /// Returns `None` if the `chrono-tz` feature is disabled, no trailing
239+ /// name is present, the name does not resolve, or the datetime part does
240+ /// not parse.
241+ #[ cfg( feature = "chrono-tz" ) ]
242+ fn parse_timestamp_with_named_tz ( s : & str ) -> Option < i64 > {
243+ let ( datetime_part, tz_name) = s. rsplit_once ( ' ' ) ?;
244+ let tz: chrono_tz:: Tz = tz_name. parse ( ) . ok ( ) ?;
245+ for fmt in & [
246+ "%Y-%m-%dT%H:%M:%S%.f" ,
247+ "%Y-%m-%dT%H:%M:%S" ,
248+ "%Y-%m-%d %H:%M:%S%.f" ,
249+ "%Y-%m-%d %H:%M:%S" ,
250+ ] {
251+ if let Ok ( naive) = NaiveDateTime :: parse_from_str ( datetime_part, fmt) {
252+ let dt = tz. from_local_datetime ( & naive) . single ( ) ?;
253+ return Some ( dt. timestamp_nanos ( ) ) ;
254+ }
255+ }
256+ None
257+ }
258+
259+ #[ cfg( not( feature = "chrono-tz" ) ) ]
260+ fn parse_timestamp_with_named_tz ( _s : & str ) -> Option < i64 > {
261+ None
262+ }
263+
226264/// Converts the naive datetime (which has no specific timezone) to a
227265/// nanosecond epoch timestamp relative to UTC.
228266fn naive_datetime_to_timestamp ( s : & str , datetime : NaiveDateTime ) -> Result < i64 > {
@@ -559,6 +597,37 @@ mod tests {
559597 Ok ( ( ) )
560598 }
561599
600+ #[ cfg( feature = "chrono-tz" ) ]
601+ #[ test]
602+ fn string_to_timestamp_named_timezone ( ) -> Result < ( ) > {
603+ // A trailing IANA timezone name is resolved and the wall-clock time
604+ // converted to a UTC instant. `America/Los_Angeles` is UTC-7 (PDT)
605+ // in June, so 13:42:29 local is 20:42:29 UTC. PostgreSQL accepts this
606+ // trailing IANA tz name.
607+ const UTC : i64 = 1_781_556_149_000_000_000 ; // 2026-06-15T20:42:29Z
608+
609+ assert_eq ! (
610+ UTC ,
611+ parse_timestamp( "2026-06-15 13:42:29 America/Los_Angeles" ) ?
612+ ) ;
613+ // `T` separator is also accepted.
614+ assert_eq ! (
615+ UTC ,
616+ parse_timestamp( "2026-06-15T13:42:29 America/Los_Angeles" ) ?
617+ ) ;
618+ // Fractional seconds with a named timezone.
619+ assert_eq ! (
620+ UTC + 190_855_000 ,
621+ parse_timestamp( "2026-06-15 13:42:29.190855 America/Los_Angeles" ) ?
622+ ) ;
623+ // UTC named timezone leaves the wall-clock time unchanged.
624+ assert_eq ! (
625+ 1_781_530_949_000_000_000 , // 2026-06-15T13:42:29Z
626+ parse_timestamp( "2026-06-15 13:42:29 UTC" ) ?
627+ ) ;
628+ Ok ( ( ) )
629+ }
630+
562631 /// Interprets a naive_datetime (with no explicit timezone offset)
563632 /// using the local timezone and returns the timestamp in UTC (0
564633 /// offset)
0 commit comments