Skip to content

fix: handle ambiguous and non-existent local times#3865

Merged
parthchandra merged 7 commits intoapache:mainfrom
matthewalex4:fix-ambiguous-time-early
Apr 10, 2026
Merged

fix: handle ambiguous and non-existent local times#3865
parthchandra merged 7 commits intoapache:mainfrom
matthewalex4:fix-ambiguous-time-early

Conversation

@matthewalex4
Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Closes #3864.

Rationale for this change

timestamp_ntz_to_timestamp panics when a local time is ambiguous or non-existent due to DST transitions. Further explanation in issue description.

What changes are included in this PR?

  • The result of from_local_datetime(...) is handled for cases of ambiguous and non-existent local times.
  • For ambiguous local times we choose the earlier option
  • For non-existent local times we shift to an hour before, convert and shift back

How are these changes tested?

Unit tests provided for both ambiguous and non-existent local times.

@matthewalex4 matthewalex4 changed the title fix: handle ambiguous and non-existent local times in timestamp_ntz_to_timestamp fix: handle ambiguous and non-existent local times Apr 1, 2026
LocalResult::Ambiguous(dt, _) => dt,
LocalResult::None => {
// Interpret nonexistent local time by shifting from one hour earlier.
let shift = TimeDelta::hours(1);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 1-hour shift works for standard DST transitions, but some timezones have non-standard gaps (e.g., Australia/Lord_Howe has a 30-minute DST transition). Yes, this is an edge case so it may be fine to ignore, but we should at least document this as an incompatibility.

https://www.timeanddate.com/time/change/australia/lord-howe-island

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment in d622e44

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm handling the DST gap in #3884. Perhaps you can wait for this to pass CI and then re-use.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider doing something like in string.rs parse_timestamp_to_micros. Something like -

  LocalResult::None => {
      let probe = local_datetime - chrono::Duration::hours(3);
      let pre_offset = match tz.from_local_datetime(&probe) {
          LocalResult::Single(dt) => dt.offset().fix(),
          LocalResult::Ambiguous(dt, _) => dt.offset().fix(),
          // Cannot determine offset – return some safe fallback or propagate error
          LocalResult::None => {
              // Extreme edge case; fall back to UTC interpretation
              local_datetime.and_utc().with_timezone(tz)
          }
      };
      let offset_secs = pre_offset.local_minus_utc() as i64;
      let utc_naive = local_datetime - chrono::Duration::seconds(offset_secs);
      utc_naive.and_utc().with_timezone(tz)
  }

This eliminates the unwrap(), handles non-standard DST correctly, and reuses a pattern that already has test coverage in Comet.


#[test]
fn test_timestamp_ntz_to_timestamp_handles_non_existent_time() {
let result = std::panic::catch_unwind(|| {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do these tests still need catch_unwind?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No not necessary, removed in d622e44

@andygrove
Copy link
Copy Markdown
Member

Thanks @matthewalex4. Could you also add an end-to-end Spark test, perhaps in CometTemporalExpressionSuite?

@matthewalex4
Copy link
Copy Markdown
Contributor Author

Thanks @matthewalex4. Could you also add an end-to-end Spark test, perhaps in CometTemporalExpressionSuite?

Yes, have added a test in 998f975

LocalResult::Ambiguous(dt, _) => dt,
LocalResult::None => {
// Interpret nonexistent local time by shifting from one hour earlier.
let shift = TimeDelta::hours(1);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider doing something like in string.rs parse_timestamp_to_micros. Something like -

  LocalResult::None => {
      let probe = local_datetime - chrono::Duration::hours(3);
      let pre_offset = match tz.from_local_datetime(&probe) {
          LocalResult::Single(dt) => dt.offset().fix(),
          LocalResult::Ambiguous(dt, _) => dt.offset().fix(),
          // Cannot determine offset – return some safe fallback or propagate error
          LocalResult::None => {
              // Extreme edge case; fall back to UTC interpretation
              local_datetime.and_utc().with_timezone(tz)
          }
      };
      let offset_secs = pre_offset.local_minus_utc() as i64;
      let utc_naive = local_datetime - chrono::Duration::seconds(offset_secs);
      utc_naive.and_utc().with_timezone(tz)
  }

This eliminates the unwrap(), handles non-standard DST correctly, and reuses a pattern that already has test coverage in Comet.

@parthchandra parthchandra merged commit 6565de9 into apache:main Apr 10, 2026
167 of 168 checks passed
@parthchandra
Copy link
Copy Markdown
Contributor

Merged. Thank you @matthewalex4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

timestamp_ntz_to_timestamp panics on ambiguous or non-existent local times (DST transitions)

3 participants