rails: timestamp schema migration on postgres doesn't include timezone

I’ve come to a very interesting problem that we could do better to prevent others from having the same issue, related to how rails create “timestamp” fields on PostgreSQL.

Standard migrations will generate a “timestamp without timezone” field, which should be fine for any non database vendor specific use-cases (rails simple queries).

The problem is that if you want to use timezone conversions like “AT TIME ZONE” on postgres, you get a very confusing behavior:

For timestamp with time zone, the internally stored value is always in UTC (Universal Coordinated Time, traditionally known as Greenwich Mean Time, GMT). An input value that has an explicit time zone specified is converted to UTC using the appropriate offset for that time zone. If no time zone is stated in the input string, then it is assumed to be in the time zone indicated by the system’s TimeZone parameter, and is converted to UTC using the offset for the timezone zone.

Conversions between timestamp without time zone and timestamp with time zone normally assume that the timestamp without time zone value should be taken or given as timezone local time. A different time zone can be specified for the conversion using AT TIME ZONE.

See: 8.5.1.3. Time Stamps at http://www.postgresql.org/docs/9.3/static/datatype-datetime.html

This is the expected behavior of AT TIME ZONE: (it will do the timezone calculations to convert any timestamp data to the one specified).

show timezone;
 TimeZone
----------
 UTC

SELECT NOW(), NOW() at time zone 'America/Sao_Paulo';
             now              |         timezone
------------------------------+---------------------------
 2015-08-04 17:18:32.34449+00 | 2015-08-04 14:18:32.34449

FYI: America/Sao_Paulo (BRT) is UTC-3.

Querying a recently created model for their “created_at” field, and a version using “AT TIME ZONE” I get:

SELECT created_at, created_at AT TIME ZONE 'America/Sao_Paulo' AS created_at_timezone FROM "mytable" ORDER BY id DESC LIMIT 1;

    id    |         created_at         |      created_at_timezone
----------+----------------------------+-------------------------------
 36597068 | 2015-08-04 17:26:37.551733 | 2015-08-04 20:26:37.551733+00

Instead of -3, what I’m observing is a +3. This is because “created_at” doesn’t include the timezone information, and therefore, according to PostgreSQL documentation, it’s using server machine timezone for the calculation.

I can’t have access to that server (heroku postgres instance), but I suspect it’s using CST (which is UTC-6).

I can define the timezone using a subquery like the following example:

SELECT t.created_at, t.created_at AT TIME ZONE 'America/Sao_Paulo' AS created_at_timezone FROM (SELECT created_at AT TIME ZONE 'UTC' as created_at FROM "mytable" ORDER BY ID desc) as t LIMIT 1;

          created_at           |    created_at_timezone
-------------------------------+----------------------------
 2015-08-04 17:26:37.551733+00 | 2015-08-04 14:26:37.551733

There is currently a workaround to create database timestamp with timezone using migrations DSL: http://stackoverflow.com/questions/19207856/railshow-to-create-a-time-column-with-timezone-on-postgres

What I propose is that we create a flag for whether you want to create a timestamp “with timezone or not”, or default it to create with timezone, as there is no bad side-effects

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 15 (8 by maintainers)

Commits related to this issue

Most upvoted comments

We are facing this problem in the company I work.

We are using Metabase (http://www.metabase.com) for reports and charts, directly connected to the rails database.

Since timestamp without zone does not store any information about time zone at all, other-than-rails clients are not able to convert the timestamps to another time zone. Actually, they don’t even know what time zone is being used. They are “forced” to assume that the timestamps are already using the desired time zone, which causes problems if you are using any other-than-UTC timezone.

From Metabase docs:

Make sure all of your database columns are properly setup to include timezone awareness.

In this example, setting the session time zone in a PostgreSQL connection doesn’t affect timestamp without zone columns:

  • created_at is timestamp without zone
  • updated_at is timestamp with zone
  • both timestamps were stored by rails as UTC
mydb => show timezone;
 TimeZone
----------
 UTC
(1 row)

mydb=> select created_at, updated_at from orders limit 1;
         created_at         |          updated_at
----------------------------+-------------------------------
 2016-06-03 15:06:35.021706 | 2016-06-03 15:06:35.672491+00
(1 row)

mydb=> set timezone='US/Eastern';
SET
mydb=> select created_at, updated_at from orders limit 1;
         created_at         |          updated_at
----------------------------+-------------------------------
 2016-06-03 15:06:35.021706 | 2016-06-03 11:06:35.672491-04
(1 row)

Note the beautiful +00 and -04 on updated_at column.

As a solution, we are planning on modifying all datetime columns to timestamp with zone and also change active record default datetime type for PostgreSQL:

  1. Generate all necessary “alter table” statments:
select 'ALTER TABLE '
  || table_name
  || ' ALTER '
  || column_name
  || ' TYPE timestamptz USING '
  || column_name
  || ' AT TIME ZONE ''UTC'';'
  from information_schema.columns
  where table_schema = 'public'
  and data_type = 'timestamp without time zone';

Result example:

...
ALTER TABLE orders ALTER updated_at TYPE timestamptz USING updated_at AT TIME ZONE 'UTC';
...
  1. Change active record default for future migrations:
# config/initializers/active_record.rb
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::
  NATIVE_DATABASE_TYPES[:datetime] = {name: "timestamp with time zone"}

Extracted from http://nandovieira.com/working-with-dates-on-ruby-on-rails

We are running those changes on a test server and we haven’t noticed any problem so far.

EDIT: We’ve executed this two weeks ago, everything working properly, no issues so far.

As a followup to this, I think the default behavior of Rails is somewhat incompatible with most other tools. An “out of the box” datetime (timestamp without zone written as UTC) field in Rails will not be parsed correctly in a at least a few other postgres clients I’ve seen. The ruby pg gem doesn’t parse it correctly, the sequel gem doesn’t, and nodejs pg module does not. And when I say it “doesn’t parse it correctly”, I mean Rails writes a UTC timestamp, and the other postgres clients are setup to always parse it as a local timestamp. I’m not sure which is correct, but it doesn’t seem ideal to create data that doesn’t play nice with other clients. I’m sure there’s a good reason for it, I just ran into this years after having it in place when we wanted to read Rails-created data from a nodejs app.

This behavior is the same as mysql database using datetime which also ignores time zone. Default options look like this:

rails mysql (tz aware?) postgresql (tz aware?)
datetime datetime (no) timestamp without time zone (no)
timestamp timestamp (yes) timestamp without time zone (no)
timestamptz error timestamp with time zone (yes)

If I understand it correctly, mysql’s timestamp (and thus rails’) is in violation of ansi sql. And timestamps definition should use timestamptz (or whatever general equivalent is) by default instead of datetime if aiming for better interoperability with non-rails db access.

I’m running into this as well. I’d vote for Rails defaulting timestamps to ‘timestamp with time zone’ – especially for the created_at and updated_at fields – I can’t see any downsides.