Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I set the time in postgres in order to test timezone behaviour using RSpec?

I want to be able to test the behaviour of a scheduler component across different timezones. However, the functionality to trigger scheduled behaviour uses time based queries within postgres:

e.g.

# find reminders which have not been sent for "today" in the local date
Schedule.where('evening_reminder_last_sent_on_local_date != 
                                     DATE( NOW() AT TIME ZONE time_zone )')

I would like to be able to test this behaviour in RSpec and ensure that it plays correctly through the day and that if I send a Japanese user a reminder at 1am UTC on the 25th Dec, then at 10pm UTC, their reminder for "today" will show up as not have been sent (since it's about 7am the next day in Japan).

However, in order to do this I need to be able to set the datetime in postgres. Is this possible?

Please note... this is not about stubbing Rails' time

The challenge is not to stub the time in Rails - I know how to do that. The problem is how to set the time in Postgres.

like image 460
Peter Nixey Avatar asked Dec 01 '13 16:12

Peter Nixey


People also ask

How does Postgres store timestamp with timezone?

The timestamptz datatype is a time zone-aware date and time data type. PostgreSQL stores the timestamptz in UTC value. When you insert a value into a timestamptz column, PostgreSQL converts the timestamptz value into a UTC value and stores the UTC value in the table.

Does Postgres store dates as UTC?

PostgreSQL assumes your local time zone for any type containing only date or time. All timezone-aware dates and times are stored internally in UTC .

How is time stored in PostgreSQL?

PostgreSQL doesn't store the time zone they come from with your timestamp. Instead it converts to and from the input and output timezone much like we've seen for text with client_encoding.

What is timestamp in PSQL?

The PostgreSQL Timestamp data type is used to store the time and date values for a specified column. We used different TIMESTAMP functions, for example, NOW(), CURRENT_TIMESTAMP, CURRENT_TIME, TIMEOFDAY(), and timezone(zone, timestamp) to enhance and handle the TIME and DATE value from the particular table.


3 Answers

Postgres uses the date / time setting of the underlying OS (at least on UNIX-like systems). To stage various timestamps, you would have to set the time of the OS, with the date command.

I would certainly not recommend that on a system that does anything else of importance. cronjobs can run haywire and other unpleasant side effects.

Instead, replace the function now() with a user-defined server-side function like:

CREATE OR REPLACE FUNCTION now_test()
  RETURNS timestamptz AS $func$SELECT '2013-12-01 12:34'::timestamptz$func$ LANGUAGE SQL;

(The above cast assumes the current time zone setting of the session. Alternatively, you can provide a time zone or time offset with the literal.)
Then you can test your code with:

Schedule.where('evening_reminder_last_sent_on_local_date != 
                             DATE(now_test() AT TIME ZONE time_zone)')

Modify the above SQL function above with various timestamps and you are good to go.

like image 147
Erwin Brandstetter Avatar answered Nov 07 '22 23:11

Erwin Brandstetter


The previous suggestion is indeed very good but as I wanted to easily test my query with Delorean gem, I came up with this workaround:

Schedule.where('evening_reminder_last_sent_on_local_date != 
               DATE( ?::timestamp AT TIME ZONE time_zone )', Time.now)

I took the previous example just for the sake, but I had to manipulate times in Postgres with the now() function. I couldn't just inject my ruby time instead without casting it with ::timestamp.

Moreover, in your case, maybe you could use timestamptz.

like image 37
pierrea Avatar answered Nov 07 '22 21:11

pierrea


I had a similar case except instead of modifying the timezones, I wanted to apply an offset. This let me synchronize postgres's responses to NOW() with my appliction's responses to datetime.now() (tampered via libfaketime).

I ended up with one statement that renamed now() to system_now(), but only if system_now() didn't already exist:

DO $$
    DECLARE
    found_func pg_proc%rowtype;
    BEGIN
    SELECT * FROM pg_proc WHERE proname = 'system_now' INTO found_func;
    IF FOUND THEN
        RAISE NOTICE 'DB Already Faked';
    ELSE
        RAISE NOTICE'Faking DB Time';
        ALTER FUNCTION now RENAME TO system_now;
    END IF;
END $$;

...and another one that redefined NOW() to include the offset:

CREATE OR REPLACE FUNCTION now() RETURNS timestamptz
AS $func$
    SELECT system_now() + INTERVAL '1225288 seconds';
$func$ LANGUAGE SQL;
like image 29
MatrixManAtYrService Avatar answered Nov 07 '22 22:11

MatrixManAtYrService