Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ActiveRecord time field issue with timezone #51679

Open
PedroAugustoRamalhoDuarte opened this issue Apr 28, 2024 · 4 comments · May be fixed by #51797
Open

ActiveRecord time field issue with timezone #51679

PedroAugustoRamalhoDuarte opened this issue Apr 28, 2024 · 4 comments · May be fixed by #51797

Comments

@PedroAugustoRamalhoDuarte
Copy link

I want to share a small bug with time field from Active Record.

Basically, the bug occurs when ActiveRecord deserialized time field's day at the turn of the day (Example: 23:00) and the current timezone is like GMT - 2. After save the record, the active record load the field in 1999-12-31 and this can break validations and comparison.

I believe it should be better storage always in 2000-01-01

Steps to reproduce

I can't get a minimal example to work, I appreciate some help with it. But is easy to reproduce with some steps:

  • Configure the timezone with Brasília in application.rb
module TimeBug
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 7.1
    config.time_zone = "Brasilia"
    
    # ...
  end
end
  • Create a model post with start_time and end_time
rails g model Post start_time:time end_time:time
  • Create this failling spec
require "test_helper"

class PostTest < ActiveSupport::TestCase
  test "the truth" do
    post = Post.new(start_time: "12:00", end_time: "22:00")
    assert_operator post.end_time, :>, post.start_time
    post.save!
    post.reload
    assert_operator post.end_time, :>, post.start_time
  end
end
  • Output
#<Post id: nil, start_time: "2000-01-01 12:00:00.000000000 -0200", end_time: "2000-01-01 22:00:00.000000000 -0200", created_at: nil, updated_at: nil>
#<Post id: 980190963, start_time: "2000-01-01 12:00:00.000000000 -0200", end_time: "1999-12-31 22:00:00.000000000 -0200", created_at: "2024-04-24 07:51:12.682996000 -0300", updated_at: "2024-04-24 07:51:12.682996000 -0300">
F

Failure:
PostTest#test_the_truth [test/models/post_test.rb:22]:
Expected Fri, 31 Dec 1999 22:00:00.000000000 -02 -02:00 to be > Sat, 01 Jan 2000 12:00:00.000000000 -02 -02:00.

Expected behavior

  • Expected to load to active record both time in 2000-01-01

Actual behavior

System configuration

Rails version: 7.1.3.2

Ruby version: 3.3.9

Gem PG: 1.5.6

@zenspider
Copy link
Contributor

I tried to help with this on discord... we couldn't get the time_zone config to stick. ENV["TZ"] and the like didn't help either. Is there an example of a single file bug repro that includes tz changes?

@matthewd
Copy link
Member

I don't think the linked test matches/describes quite this behaviour -- that is showing a conversion between stored and reported time zones, which shifts the result back a few hours (ending up on the 31st). Here the actual time component is correct, but a full day back.

🤔

Oh, I think I understand.

The test behaviour feels intuitively correct to me: the stored value is midnight, and then when it's loaded in an offset timezone, the value is "N hours before midnight".

But here, the application is consistently working in Brasilia TZ, so it intends to store "22:00". Because the DB is using UTC, though, that ends up stored as midnight on the 2nd... truncated to 00:00 by the time field. And then when loaded, the DB said "midnight on the 1st, UTC"... which after TZ correction, is 2 hours before that, on the 31st.

I think this a fundamental problem with having the application and database timezone differing while storing unanchored time values: the storable range is represented by Jan 1 in the DB zone (because that's what the DB can represent), and that's what we're trying to translate for the application. If you look at the raw values stored in the DB at the midpoint of the test, I think that will show why end < start is actually quite defensible, and the opposite would be more surprising -- the problem happens back during the save, where time's storage limitations truncate away the future-ness.

I don't remember how timetz fits in here, but it might help?

@mkbehbehani
Copy link

@PedroAugustoRamalhoDuarte if your goal is to compare Time of Day without a specific date, one option is to use the Tod gem, particularly the ActiveRecord integration. Please ignore this if your goal is to compare on specific dates.

Time of Day comparison details

Expectation

We want to work with a time of day, no date involved. This comparison should be true: end_time 22:00 > start_time 12:00

Problem

The Ruby Time and ActiveSupport::TimeWithZone include date. They represent a specific point in time, not the concept of Time of Day.

Time.new => 2024-04-29 17:35:36.866882654 +0000

As @matthewd pointed out there is also conversion into/out of Postgres type which breaks the comparison in certain times/timezones as the UTC offset gets applied and then stored. Even if you set up the column to persist as a Postgres time type (time of day-no date) instead of timestamp (both date and time), you may run into this as it is cast back into a Time or TimeWithZone object and a date is applied.

Approach I've used

  • Using the ToD :time_only ActiveRecord attribute for Time of Day representation in application logic
  • In Postgres, using the time column type.
  • Timezone config for both Rails and Postgres set to UTC and performing changes into desired local timezone before read/write/comparison using .in_time_zone(location_timezone).

@PedroAugustoRamalhoDuarte
Copy link
Author

Thanks for the fast response!!

Thanks, @mkbehbehani, for the solution's approach. I am using the third approach right now, and it's working nicely. And thanks for express my problem better.

I think we can improve the rails default conversion for time field.

When we work with time, even though we have to deal with the entire date, we only want to deal with time. So I believe that we can work on the conventions so that even with timezones configured in the application, the dates in the time fields always occur on January 1st to have a better integration with validations and calculations.

I understand is not a big deal for the framework, is a very particularly use case, but if you guys agreed to me, I can make a PR

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 a pull request may close this issue.

4 participants