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

Use UTC when encoding datetime values into cursor. #92

Closed

Conversation

xijo
Copy link

@xijo xijo commented Oct 8, 2022

Hello,

Rails stores datetime values in the DB as UTC as a default. When using a custom order_by on a datetime field, e.g. updated_at however, the order_field_value is encoded in the local time zone, which leads to faulty pagination.

This PR fixes this issue by always treating the value as UTC if it responds to utc. To provide a meaningful test case I've change the spec setup slightly, so that it operates in a different time zone, but still treats datetimes correctly.

To some users, if they were deviating from rails defaults and stored datetimes in local time, this might be a breaking change. However, I'd assume that majority of rails applications adheres to the default.

@aaronsama
Copy link
Contributor

Hello @xijo and thanks for opening a PR. I did a bit of research and it looks like only attributes of type TIMESTAMP are stored using UTC in MySQL > 5 (reference: https://stackoverflow.com/a/602038). That would be the case of a created_at column for example, which is usually added as follows in a migration (reference: https://edgeguides.rubyonrails.org/active_record_migrations.html)

Other types such as datetime and time are "timezone aware" and are converted to the local timezone when pulled from the DB (reference: https://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html)

It looks like this can be customized according to the docs I linked above, possibly making the conversion to UTC problematic. I guess this is what you mean when you say it would be a "breaking change" for some users. What about skipping the conversion to UTC if we detect that the application is set to config.active_record.time_zone_aware_attributes = false or config.active_record.default_timezone = :local? I guess our main use case for this is when a timestamp is used to generate the cursor (btw, there's no guarantee that the timestamp would be unique)

@xijo
Copy link
Author

xijo commented Oct 10, 2022

Hi @aaronsama,

according to the docs that you've linked:

By default, these values are stored in the database as UTC and converted back to the current Time.zone when pulled from the database.

Going with the principle of least surprise, having the gem work out of the box for the current rails default seems like the right thing to do, right?

As for respecting the configuration, that is a solid plan, I'll add that to the PR. 👍

Just to clarify my current use case a bit. We're storing a field content_updated_at with nanosecond precision in order to bubble up new/updated content in our feed. With that in mind, the cursor for our use case could look very differently (it would not need to include the id at all).

@xijo xijo force-pushed the fix_utc_issues_when_sorting_by_datetime branch from 336a1e8 to 6229fd1 Compare October 10, 2022 12:00
@xijo
Copy link
Author

xijo commented Oct 10, 2022

@aaronsama PR is updated accordingly, added a couple more test cases as well.

Copy link
Contributor

@aaronsama aaronsama left a comment

Choose a reason for hiding this comment

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

Thanks again for your PR. I added some comments to improve the tests a bit. In general I would try to avoid having the timezone related configuration modified for the whole suite and move it to the tests where it's needed. Please have a look if it makes sense to you.

@@ -35,6 +39,7 @@ class Post < ActiveRecord::Base; end
ActiveRecord::Migration.create_table :posts do |t|
t.string :author
t.string :content
t.datetime :created_at
Copy link
Contributor

Choose a reason for hiding this comment

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

What about adding two test cases, one for timestamps and one for a datetime attribute? They are slightly different and would better reflect a real world scenario

Suggested change
t.datetime :created_at
t.timestamps
t.datetime :some_other_datetime_field

Comment on lines +25 to +27
# Configure time_zone like it would be in rails applications
ActiveRecord::Base.time_zone_aware_attributes = true
Time.zone = 'Berlin'
Copy link
Contributor

Choose a reason for hiding this comment

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

I would rather mock the response values or change the setup only in the tests where we need it rather than changing the configuration for the whole test suite. Something like the code below would be enough probably.

  # This can be different for different contexts
  before do
    ActiveRecord::Base.time_zone_aware_attributes = true
    Time.zone = 'Berlin'
  end

  after do
    # what you did in the file above
  end

It looks like stubbing Time.zone can be problematic but I found this: https://stackoverflow.com/a/26741932

Comment on lines +79 to +91
it 'respects the time_zone_aware_attributes setting' do
ActiveRecord::Base.time_zone_aware_attributes = false

expect(decoded_time).to eq record.created_at
expect(decoded_time).not_to be_utc
end

it 'respects the default_timezone setting' do
ActiveRecord::Base.default_timezone = :local

expect(decoded_time).to eq record.created_at
expect(decoded_time).not_to be_utc
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Since you are altering the context in which these examples are run I propose moving them to their own context. See the comment below.

Something like

context 'when timezone aware attributes are disabled' do
  before do
    ActiveRecord::Base.time_zone_aware_attributes = false
  end

  # your examples here
end

context 'when using :local as default timezone' do
  before do
    ActiveRecord::Base.default_timezone = :local
  end

  # your examples here
end

@@ -51,6 +51,45 @@
expect(decoded.order_field_value).to eq record.author
end
end

context 'when ordering by created_at' do
Copy link
Contributor

Choose a reason for hiding this comment

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

I would try to make this more generig

Suggested change
context 'when ordering by created_at' do
context 'when ordering by a timestamp attribute' do

@aaronsama
Copy link
Contributor

This is addressed in #142 now. Closing this PR.

@aaronsama aaronsama closed this Oct 5, 2023
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.

2 participants