4

In our app we're storing datetimes that belong to many different timezones. We decided to use the Joda LocalDateTime type - so that the user always gets literally whatever they entered in the first place. This is exactly what we need.

Internally we know which timezone the user belongs to - so when they enter a datetime we do a check like this:

dateTimeZone.isLocalDateTimeGap(localDateTime)

If that datetime does not exist in their timezone (it's in the daylight-savings gap) we display an error message that the date is not correct, thus preventing incorrect datetimes from being stored in the DB.

For storing we're using a timestamp column. Problems start when the user-entered datetime exists in their timezone but does not exist in the database timezone (Europe/Berlin). E.g. when I store LocalDateTime 2015-03-29 02:30:00 from the Europe/London timezone (this is valid - in London the gap is between 01:00 and 02:00), PostgreSQL shifts the hour by 1 and saves it as 2015-03-29 03:30:00.

What to do? Is there a way to tell PostgreSQL not do anything regarding timezones and just store datetimes literally as Joda represents them? (other than storing them as strings ;))

machinery
  • 3,793
  • 4
  • 41
  • 52
  • 1
    In Postgres, `timestamp` is not time zone aware. So there should be no reason that Postgres will shift the time. Can you post your code for how you save the value to the database? Also - what version of Postgres are you running? – Matt Johnson-Pint Oct 03 '14 at 16:03
  • 1
    Make at least sure that the server where PostgreSQL is running uses GMT+00:00 as fixed offset so there cannot be any gap-related manipulation of local timestamps. Furthermore, is your jdbc-driver local-timezone-sensitive? If yes, then there might be some configuration option for using fixed offsets. – Meno Hochschild Oct 05 '14 at 16:48
  • @MattJohnson & Meno Hochschild - thank you both for you comments :) Matt's remark "timestamp is not time zone aware" helped me solve the problem. It was apparently caused by my editor (SQLWorkbench) which was shifting the timestamp. When I displayed the timestamp in pgAdmin / psql it wasn't shifted. Matt, if you add this remark as an answer, I will accept it. – machinery Oct 08 '14 at 07:43
  • For clarity, be very specific with the full formal name in describing either `TIMESTAMP WITHOUT TIME ZONE` or `TIMESTAMP WITH TIME ZONE` column type in Postgres or any other SQL standard compliant database. Their meaning and behavior is *entirely* different. – Basil Bourque Dec 31 '17 at 02:47

2 Answers2

3

In PostgreSQL 7.3 and higher, timestamp is equivalent to timestamp without time zone. That data type is not time zone aware. It stores only a date and time. If you are finding it shifted, then it might be related to the code or tools you are using to store or retrieve the data.

Note that before version 7.3, timestamp was equivalent to timestamp with timezone. This is mentioned in the first note-box in the documentation here.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
0

Postgres offers two date-time types per the SQL standard. The standard barely touches on the topic unfortunately, so the behavior described here is specific to Postgres. Other databases may behave differently.

  • TIMESTAMP WITHOUT TIME ZONE
    Stores just a date and a time-of-day. Any time zone or offset-from-UTC passed is ignored.
  • TIMESTAMP WITH TIME ZONE
    First adjusts the passed date+time using its passed zone/offset to get a value in UTC. The passed zone/offset is then discarded after the adjustment is made; if needed, you must store that original zone/offset information in a separate column yourself.

Be aware that TIMESTAMP WITHOUT TIME ZONE does not represent an actual moment, does not store a point on the timeline. Without the context of a zone or offset, it has no real meaning. It represents a range of possible moments over a span of about 26-27 hours. Good for problems such as storing a appointment far enough out in the future that the time zone rules may be changed before its arrival. Also good for problems such as “Christmas starts after midnight on December 25 this year”, where you mean a different moment in time in each zone with each zone westward arriving later and later in succession.

When recording actual moments, specific points on the timeline, use TIMESTAMP WITH TIME ZONE.

The modern approach in Java uses the java.time classes rather than either the Joda-Time library or the troublesome old legacy date-time classes bundled with the earliest versions of Java.

TIMESTAMP WITHOUT TIME ZONE

For TIMESTAMP WITHOUT TIME ZONE, the equivalent class in java.time is LocalDateTime for a date and time-of-day without any offset or zone.

As others pointed out, some tools may dynamically apply a time zone to the retrieved value in a misguided and confusing albeit well-intentioned anti-feature. The following Java code will retrieve your true date-time value sans zone/offset.

Requires a JDBC driver compliant with JDBC 4.2 or later to directly work with java.time types.

LocalDateTime ldt = myResultSet.getObject( … , LocalDateTime.class ) ;  // Retrieving a `TIMESTAMP WITHOUT TIME ZONE` value.

To insert/update database:

myPreparedStatement.setObject( … , ldt ) ;  // Inserting/updating a `TIMESTAMP WITHOUT TIME ZONE` column.

TIMESTAMP WITH TIME ZONE

Your discussion of time zones suggests you are concerned with actual moments on the timeline. So you should absolutely be using TIMESTAMP WITH TIME ZONE instead of TIMESTAMP WITHOUT TIME ZONE. You should not be messing about with Daylight Saving Time (DST) gaps and such. Let java.time and Postgres do that work for you, with much better code already written and tested.

To retrieve:

Instant instant = myResultSet.getObject( … , Instant.class ) ;  // Retrieving a `TIMESTAMP WITH TIME ZONE` value in UTC.
ZonedDateTime zdt = instant.atZone( ZoneId.of( "Africa/Tunis" ) ) ;  // Adjusting from a UTC value to a specific time zone.

To insert/update database:

myPreparedStatement.setObject( … , zdt ) ;  // Inserting/updating a `TIMESTAMP WITH TIME ZONE` column.

To retrieve from database:

Instant instant = myResultSet.getObject( … , Instant.class ) ;

E.g. when I store LocalDateTime 2015-03-29 02:30:00 from the Europe/London timezone

No, no, no. Do not work this way. You are misusing the types of both Java and Postgres.

If the user entered 2015-03-29 02:30:00 intended to represent a moment in Europe/London time zone, then parse as a LocalDateTime and immediately apply a ZoneId to get a ZonedDateTime.

To parse, replace the SPACE in the middle with a T to comply with ISO 8601 standard formatting used by default in the java.time classes.

String input = "2015-03-29 02:30:00".replace( " " , "T" ) ;
LocalDateTime ldt = LocalDateTime.parse( input ) ;
ZoneId z = ZoneId.of( "Europe/London" ) ;
ZonedDateTime zdt = ldt.atZone( z ) ; 

To see that same moment in UTC, extract a Instant. The Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds (up to nine (9) digits of a decimal fraction).

Instant instant = zdt.toInstant() ;

Pass the instant via JDBC for storage in the database in a TIMESTAMP WITH TIME ZONE.

myPreparedStatement.setObject( … , instant ) ;

Use objects, not strings

Note that all my code here is using java.time objects to exchange data with the database. Always use these objects rather than mere strings for exchanging date-time values.


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154