A few questions regarding our handling of date/time came up in our internal Slack recently:
- I would like UTC timestamp without any timezone; should use local_datetime?
- Or should I use datetime and expect nobody messes up with timezone?
- If I need a timestamp, like expiration time of something. Should this be a timezone-aware datetime?
- Should I assume it's UTC when returned from database or should I enforce (convert) timezone when formatting?
- I would expect
local_datetime
to be timezone-aware.
Since this is quite a complex topic I'd like to start by explaining how EdgeDB works with date/time compared to Postgres.
EdgeDB
In EdgeDB there is a timezone aware type std::datetime
and a naive date/time type std::local_datetime
.
std::datetime
is stored internally in UTC and binary-wise it's identical to std::local_datetime
. The fact that std::datetime
values are time zone aware is encoded into their type. E.g. when a user receives a std::datetime
value they:
-
Know that they are dealing with a time zone aware date/time.
-
If they want to render this date time to string in their front-end code they have to specify the target time zone. E.g. to render a std::datetime
value to a string via EdgeQL the user would use std::to_str
function that would require the user to pass the timezone they want the time to be rendered in. Usually that timezone would be the timezone of the current user that's interacting with the date.
-
EdgeQL is strict w.r.t. separating std::datetime
from std::local_datetime
. For instance it's not possible to subtract std::datetime
from std::local_datetime
— the operation is undefined. Subtracting std::datetime
from std::datetime
or std::local_datetime
from std::local_datetime
is allowed and would produce a std::duration
.
-
When a user submits a std::datetime
value to the database it's their responsibility to let the DB know how to convert it to UTC. There's no magical config setting (unlike in Postgres) that would automatically let naive date/time be converted to time zone aware date/time.
Here's a couple of use cases to understand the naming motivation:
-
datetime
is a globally recognizable point in time. Example: space shuttle launch time; file creation time; when a log or audit record was created; precise moment of when a financial transaction has been committed.
-
local_datetime
is something that makes sense regardless of time zone. Examples: milk expiry date; alarm clock wake up time; arrival time on air/train tickets is always in local time of the destination.
💡 Maybe we should add this ^^ to the docs.
PostgreSQL
Quoting Postgres documentation:
PostgreSQL endeavors to be compatible with the SQL standard definitions for typical usage. However, the SQL standard has an odd mix of date and time types and capabilities.
There are two types in Postgres that are interesting for us in the context of this discussion: timestamp
and timestamptz
(it's an alias for SQL's timestamp with time zone
):
-
timestamp
is used to store naive date/time, e.g. when time zones aren't applicable or not known.
-
timestamptz
is used to store time zone aware date/time. It is stored on the server in UTC. As in EdgeDB, in Postgres, they are identical values when stored on the disk, meaning that the difference between the two is purely their type id (OID).
So far so good, Postgres date/time handling looks just like EdgeDB. The key difference is that in Postgres there's a server config setting TimeZone
that is used to automatically convert time zone aware and naive date/time to each other. Examples:
-
if a user submits a text date/time without timezone to timezonetz
cast Postgres will use the config setting and convert it silently. When the user queries the submitted value back, it might be different from what was submitted because of that transformation, leaving the user confused.
-
if a user subtracts a timezone
value from a timezonetz
value, Postgres will use the config setting to silently convert timezone
to timezonetz
. Again, this can lead to confusion, as the result would look totally different from what a naive user assumed.
EdgeDB does not have automatic conversion. To submit a std::datetime
value a time zone must be specified along with it. Implicit mixing of time zone aware and naive date/time values is prohibited.
Q&A
- I would like UTC timestamp without any timezone; should use local_datetime?
You should use std::datetime
. Just point out to EdgeDB that it's already in UTC.
- Or should I use datetime and expect nobody messes up with timezone?
You should indeed use std::datetime
. You should make sure that your frontend code renders your date/time values either in: local user's time zone; or in UTC. The format is up to you and your UI requirements, EdgeDB does not care.
- If I need a timestamp, like expiration time of something. Should this be a timezone-aware datetime?
Depends. If it's the expiration time of a carton of milk I'd say you should use std::local_datetime
. If it's the expiration time of evaluation period of your SaaS offering it should be std::datetime
-- that way the user will know the precise time no matter where they currently are, in London or in New York.
- Should I assume it's UTC when returned from database or should I enforce (convert) timezone when formatting?
For std::datetime
— yes!
For std::local_datetime
— it can be whatever, the database does not know. Only the developer knows and can use it correspondingly.
The rule of thumb here is that if the date/time value is meant to be rendered using different time/zones it should always be std::datetime
.
- I would expect
local_datetime
to be timezone-aware.
Yes, I can see where the confusion is coming from. See the💡-paragraph section above for our motivation of naming it that way.
We might want to consider renaming local_datetime
back to naive_datetime
. At the very least our documentation must be improved.
documentation