Search code examples
ruby-on-railspostgresqldatetimedstrails-postgresql

TimeZone and DST in Rails and PostgreSQL


Background

Article model with default created_at column

Rails config.time_zone = 'Warsaw'

I've got an article with created_at = local time 2012-08-19 00:15 (2012-08-18 22:15 in UTC).

Goal

To receive all articles created in 2012-08-19 (in local time).

My (not working properly) solution

Article.where(
  "date_trunc('day', created_at AT TIME ZONE '#{Time.zone.formatted_offset}')
   = '#{Date.civil(2012, 8, 19)}'"
)

Which generates SQL:

SELECT "articles".* FROM "articles"
WHERE (date_trunc('day', created_at AT TIME ZONE '+01:00') = '2012-08-19')

And returns an empty set. But if I run the same query in psql it returns an article ... which confuses me.

Question

What am I doing wrong and how to fix it?


Solution

  • Goal: To receive all articles created in 2012-08-19 (in local time).

    '+01:00' (like you use it) is a fixed time offset and cannot take DST (Daylight Saving Time) into account. Use a time zone name for that (not an abbreviation). These are available in PostgreSQL:

    SELECT * FROM pg_timezone_names;
    

    For Warsaw this should be 'Europe/Warsaw'. The system knows the bounds for DST from its stored information and applies the according time offset.


    Also, your query can be simplified.

    As created_at is a timestamp [without time zone], the values saved reflect the local time of the server when the row was created (saved internally as UTC timestamp).

    There are basically only two possibilities, depending on the time zone(s) of your client.

    1. Your reading client runs with the same setting for timezone as the writing client: Just cast to date.

      SELECT *
      FROM   articles
      WHERE  created_at::date = '2012-08-19';
      
    2. Your reading client runs with a different setting for timezone than the writing client: Add AT TIME ZONE '<tz name of *writing* client here>'. For instance, if that was Europe/Warsaw, it would look like:

      ...
      WHERE (created_at AT TIME ZONE 'Europe/Warsaw')::date = '2012-08-19';
      

    The double application of AT TIME ZONE like you have it in your posted answer should not be necessary.

    Note the time zone name instead of the abbreviation. See:

    If you span multiple time zones with your application ..

    .. set the column default of created_at to now() AT TIME ZONE 'UTC' - or some other time zone, the point being: use the same everywhere.

    .. or, preferably, switch to timestamptz (timestamp with time zone).