In a Rails 2 application, there are several settings that affect how timezones are written, parsed, and escaped. When the database isn’t in UTC, this can lead to discrepancies in the values that are stored and retrieved from the database.
For background reading, take a look at the “Easier Timezones” article introducing timezone support in Rails.
The Default: UTC
The default generated configuration assumes that the database is running in UTC,
and all times are handled accordingly. From the generated
# Set Time.zone default to the specified zone and make Active Record # auto-convert to this zone. # Run "rake -D time" for a list of tasks for # finding time zone names. config.time_zone = 'UTC'
Assigning a value to
config.time_zone also sets these additional ActiveRecord
ActiveRecord::Base.default_timezone = :utc
ActiveRecord::Base.time_zone_aware_attributes = true
Fortunately, if you do nothing else, this will write valid data to and from the
database. But there’s a problem: what if your server, or your testing
environment, or your local
script/console aren’t also in UTC?
Post.all(:conditions => [ "created_at > ? AND created_at < ?", Time.now - 1.hour, Time.now ])
Whoops. The actual SQL generated will use
Time.now.to_s(:db), which is local
# local timezone is -0600, MDT now = Time.now # => Wed Apr 20 09:37:47 -0600 2011 now.to_s(:db) # => "2011-04-20 09:37:47" # but the correct time is now.utc.to_s(:db) # => "2011-04-20 15:37:47"
To produce the desired behavior sans patching, you can use
Post.all(:conditions => [ "created_at > ? AND created_at < ?", Time.zone.now - 1.hour, Time.zone.now ])
The fun begins when the database isn’t in UTC, and time data should be stored in the local timezone. There are certainly problems with this approach–especially when it comes to daylight savings transitions–but it’s sometimes a legacy requirement and can’t be changed.
ActiveRecord’s quoting code handles times by calling
#to_s(:db). For bare
Time objects, this merely strips the zone offset information, regardless of
whether or not the
Time is in UTC or local time.
now = Time.now # => Wed Apr 20 09:37:47 -0600 2011 now.to_s(:db) # => "2011-04-20 09:37:47" now.utc.to_s(:db) # => "2011-04-20 15:37:47"
config.time_zone is set, timestamp attributes are not bare
Time objects, they’re actually wrapped as
#to_s(:db) method always converts the underlying time to UTC:
now = Time.now # => Wed Apr 20 09:37:47 -0600 2011 Time.zone = "Mountain Time (US & Canada)" in_zone = now.in_time_zone # => Wed, 20 Apr 2011 09:37:47 MDT -06:00 in_zone.class # => ActiveSupport::TimeWithZone in_zone.to_s(:db) # => "2011-04-20 15:37:47" # same as: now.utc.to_s(:db) # => "2011-04-20 15:37:47"
Unfortunately, this means most time values are written to the database in UTC.
config.time_zone doesn’t affect this, either, as it only determines
the timezone that is used for display purposes, not how the times are serialized
and read from the database.
Additional Time Settings
To go further in depth, there are four settings that collectively determine how times are handled in Rails 2.
TZ environment variable defines which timezone
Time.now will use in its
return value. This overrides the system’s default timezone. On a system running
in Mountain time:
ENV["TZ"] # => nil Time.now # => 2011-04-11 09:45:56 -0600 ENV["TZ"] = "US/Eastern" Time.now # => 2011-04-11 11:46:03 -0400
In ruby, the regular
Time class is restricted to either system time
ENV["TZ"]) and UTC. As introduced in Rails 2.1,
Time.zone sets the
display timezone, but does not otherwise affect how times are parsed or
This setting determines how a timestamp is to be interpreted when parsed from the
database. By default, when
config.time_zone is set in
ActiveRecord::Base.default_timezone is set to
:utc. This means the times are
parsed directly as if they were UTC. When manually set to
:local, times are
parsed using the local timezone (
Changing the setting to
:local gets us part of the way: times are now read
according to the server timezone. But, as they’re still written in UTC, the
round-trip is incorrect.
Time-zone aware attributes are also enabled by default when setting
config.time_zone. When enabled, times read from the database are automatically
ActiveSupport::TimeWithZone, both when writing the attribute and
when accessing the attribute. This is designed for use in conjuction with
Time.zone settings, and is enabled by default.
The singular point in ActiveRecord that causes timezone code to fail is in the
quoted_date method in the
to_s(:db) directly, always, which ends up forcing UTC in many cases
even when it’s not appropriate.
def quoted_date(value) value.to_s(:db) end
In Rails 3, this has been fixed with more intelligent behavior:
def quoted_date(value) if value.acts_like?(:time) zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value else value end.to_s(:db) end
Jetlag started as a suite of specs to explore the range of time-related behavior in Rails, finally resulting in the backported patch and a rails plugin. You can browse the source on github, or install it as a plugin:
script/plugin install https://github.com/zerowidth/jetlag.git
If your database is not in UTC, set
ENV["TZ"] explicitly in your
config/environment.rb to match the database’s timezone, and also add the
following line after
config.time_zone = 'UTC':
config.active_record.default_timezone = :local
What about Rails 3?
Rails 3 works fine with non-UTC databases, as long as
ENV["TZ"] matches the
database’s timezone, and and
ActiveRecord::Base.default_timezone is set to
:local. This is verified via the specs in the
rails3 branch of jetlag.