Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #7338 (new defect)

Opened 1 year ago

Last modified 5 months ago

Time zone is not adhered to in ActiveRecord

Reported by: sneakin Assigned to: core
Priority: high Milestone: 2.x
Component: ActiveRecord Version: edge
Severity: critical Keywords: date time datetime activerecord utc local
Cc:

Description

At least with the SQLite3 adapter, ActiveRecord does use the correct time zone. When I had my default timezone set to :local, ActiveRecord would take a datetime from the database, which was in UTC, and place it into the local timezone. This resulted in ActiveResource doing a double conversion.

A quick example would be:

   time = Time.now
   record = Record.new(:created_on => time)
   record.time == time    # => true
   record.save
   record.reload
   record.time == time    # => false

Inspecting record's time reveals that ActiveRecord places it in the local time zone w/o converting it from the database's UTC value. What you end up with is UTC time in the local time zone. That's not a good thing, especially when you send this to ActiveResource which does its own conversion to UTC. That results in a double conversion, UTC * 2.

Attachments

local_time_zone_test_mysql.diff (1.0 kB) - added by smeade on 01/25/07 04:39:40.
local_time_zone_test_mysql.diff

Change History

01/24/07 00:31:56 changed by sneakin

Oops, the calls to Record#time should be Record#created_on

01/25/07 03:29:27 changed by rsanheim

Is this against the latest sqlite3 gem? See also Jamis' post on the recent release: http://weblog.jamisbuck.org/2007/1/13/sqlite3-ruby-1-2-0

01/25/07 04:26:19 changed by smeade

The console lines presented in this ticket do not illustrate a timezone problem. Please provide more data. Does your console snippet work as expected when AR default time is set to UTC? I doubt it.

Two things could be happending that result in the console output you posted. Because you did not post the output of the variables, it hard to tell details. So, short of details here is what "could" be happening (again - not sure without seeing a failing test case - but I can recreate this in console and will post a test case shortly).

1 - Make sure you are using DateTime as the type for created_on, correct? The convention is created_on is Date, which of course would not match a Time value.

2 - If you are storing datetime in the record, another thing could be getting you. The Ruby type Time includes microseconds. Microseconds are not saved to the database. So when you reload from the database the attribute's datetime (without microseconds) is almost always going to be different than the datetime of the Ruby variable (with microseconds).

Here's one way to check if microseconds are the problem, take them off before writing to the record.

>> timenow = Time.now 
=> Wed Jan 24 21:09:52 MST 2007
>> time_no_microsec = Time.mktime(timenow.year, timenow.month, timenow.day, timenow.hour, timenow.min, timenow.sec)      
=> Wed Jan 24 21:09:52 MST 2007
>> newsitem = NewsItem.new(:created_at => time_no_microsec)
=> #<NewsItem:0x240ee64 @new_record=true, @attributes={"updated_at"=>nil, "body"=>nil, "title"=>nil, "lock_version"=>0, "excerpt"=>nil, "broadcast_at"=>nil, "user_id"=>nil, "created_at"=>Wed Jan 24 21:09:52 MST 2007}>
>> newsitem.save
=> true
>> newsitem.reload
=> #<NewsItem:0x240ee64 @new_record=false, @errors=#<ActiveRecord::Errors:0x240c7cc @base=#<NewsItem:0x240ee64 ...>, @errors={}>, @attributes={"updated_at"=>"2007-01-24 21:11:56", "body"=>nil, "title"=>nil, "lock_version"=>"0", "id"=>"", "excerpt"=>nil, "broadcast_at"=>nil, "user_id"=>nil, "created_at"=>"2007-01-24 21:09:52"}>
>> newsitem.created_at == time_no_microsec 
=> true

In both cases, your experience points out that what is saved to the model in memory can be any ruby type. You can do this without complaint:

>> newsitem.created_at = "this is text"
=> "this is text"
>> newsitem.save  
=> true
>> newsitem.reload
=> #<NewsItem:0x240ee64 @new_record=false, @errors=#<ActiveRecord::Errors:0x240c7cc @base=#<NewsItem:0x240ee64 ...>, @errors={}>, @attributes={"updated_at"=>"2007-01-24 21:22:11", "body"=>nil, "title"=>nil, "lock_version"=>"1", "id"=>"", "excerpt"=>nil, "broadcast_at"=>nil, "user_id"=>nil, "created_at"=>nil}>
>> newsitem.created_at
=> nil

That is why before reload the values matched, because they had not yet been modified for saving to the db.

I could be all wet on this. If so, please post more info so people can investigate further.

01/25/07 04:26:58 changed by smeade

... also note my test are only with MySQL, not sqlite3. Please post a sqlite3 failing test case.

01/25/07 04:39:40 changed by smeade

  • attachment local_time_zone_test_mysql.diff added.

local_time_zone_test_mysql.diff

02/21/08 07:53:06 changed by sphivo

I'm running Edge with the current sqlite3-ruby adapter, and I'm getting the same behavior.

config.active_record.default_timezone = :utc

Here's an example table:

create_table :trolls do |t|
  t.timestamp :born_at
end

Console session showing the problem:

t = Troll.new
=> #<Troll id: nil, born_at: nil>
t.born_at = Time.now
=> Thu Feb 21 00:53:58 -0500 2008
t.save
=> true
t.reload
=> #<Troll id: 1, born_at: "2008-02-21 00:53:58">
t.born_at
=> Thu Feb 21 00:53:58 UTC 2008

(The attribute is being saved as localtime, then interpreted on load as UTC.)

Note that created_at and updated_at do *not* have this problem, because of this line, at source:/trunk/activerecord/lib/active_record/timestamp.rb@8217#L22

t = self.class.default_timezone == :utc ? Time.now.utc : Time.now

This check should be happening to all Time attributes on-save.

I can't find the obvious place to make this fix without uglying up some clean code paths, or spreading it out to the Adapters (Quoting#quoted_date).

Locally, I've worked around this with:

class ActiveRecord::Base
  protected
  def write_attribute_with_utc_fix(k,v)
    if default_timezone == :utc
      v = v.utc if v.acts_like?(:date) || v.acts_like?(:time)
    end
    write_attribute_without_utc_fix(k, v)
  end
  alias_method_chain :write_attribute, :utc_fix
end

I doubt this will work for users-at-large. I'll look into this some more as my schedule allows.