hpgsql-simple-compat-0.1.0.0: src/Database/PostgreSQL/Simple/Time.hs
-- |
-- Module: Database.PostgreSQL.Simple.Time
-- Copyright: (c) 2012-2015 Leon P Smith
-- License: BSD3
-- Maintainer: Leon P Smith <leon@melding-monads.com>
-- Stability: experimental
--
-- This module provides time types that supports positive and negative
-- infinity, as well as some functions for converting to and from strings.
--
-- Also, this module also contains commentary regarding postgresql's timestamp
-- types, civil timekeeping in general, and how it relates to
-- hpgsql-simple-compat. You can read more about PostgreSQL's date and time types
-- at <https://www.postgresql.org/docs/9.5/static/datatype-datetime.html>,
-- and the IANA time zone database at <https://en.wikipedia.org/wiki/Tz_database>.
--
-- Stack Overflow also has some excellent commentary on time, if it is a
-- wiki page or a highly upvoted question and answer. If the answer regarding
-- time has not received about a hundred upvotes at least, then the answer is
-- almost invariably completely and painfully wrong, even if it's the chosen
-- answer or the most highly upvoted answer to a question.
--
-- PostgreSQL's @timestamp with time zone@ (hereafter, @timestamptz@) can be
-- converted to Haskell's 'Data.Time.UTCTime' and 'Data.Time.ZonedTime' types,
-- because values of these types represent a self-contained, unambiguous point
-- in time. PostgreSQL's @timestamp without time zone@ (hereafter, @timestamp@)
-- can be converted to Haskell's 'Data.Time.LocalTime', because values of these
-- types are ambiguous by themselves, and require context to disambiguate.
--
-- While this behavior may be superficially counterintuitive because the
-- names might suggest otherwise, this behavior is correct. In fact,
-- the \"timezone\" language in both the postgresql and haskell types would
-- be better read as \"offset (from UTC)\", thus we have postgresql's
-- \"timestamp with offset\" corresponding to Haskell's \"time with the
-- offset \'zero\'\" and Haskell's \"time with an offset (that might be
-- nonzero)\". Similarly, postgresql's \"timestamp without an offset\"
-- corresponds to Haskell's \"local time (without an offset)\".
--
-- It's important to distinguish between an offset, a standard time, and
-- a time zone. An offset is simply a difference of a local time from UTC,
-- such as @+00@, @-05@, or @+05:30@. A standard time specifies an offset
-- (which may vary throughout the year, due to daylight savings) that a
-- region follows, such as Universal Coordinated Time (UTC), Eastern Standard
-- Time\/Eastern Daylight Time (EST\/EDT), or India Standard Time (IST).
-- And a time zone, much like a standard time, is a function from
-- timestamps to offsets.
--
-- A time zone is different from a standard time because different regions
-- inside a standard time can be governed by different civil authorities with
-- different laws and thus have different histories of civil time. An IANA
-- time zone is any region of the world that has had the same history of
-- civil time since @1970-01-01 00:00+00@.
--
-- For example, as of today, both @America\/New_York@ and
-- @America\/Indiana\/Indianapolis@ are on the EST\/EDT time standard, but
-- Indiana used to be on Central Standard Time until 1942, and did not observe
-- daylight savings time (EST only) until 2006. Thus, the choice between
-- these two time zones still matters if you are dealing with timestamps
-- prior to 2006, and could become relevant again if (most of) Indiana
-- moves back to Central Time. (Of course, if the Central to Eastern switch
-- was the only difference, then these two time zones would be the same in
-- IANA's eyes, due to their cutoff date of 1970-01-01.)
--
-- Getting back to practicalities, PostgreSQL's @timestamptz@ type does not
-- actually store an offset; rather, it uses the offset provided to calculate
-- UTC, and stores the timestamp as UTC. If an offset is not provided, the
-- given timestamp is assumed to be a local time for whatever the @timezone@
-- variable is set to, and the IANA TZ database is consulted to calculate an
-- offset from UTC for the time in question.
--
-- Note that while most (local timestamp, time zone) pairs correspond to exactly
-- one UTC timestamp, some correspond to two UTC timestamps, while others
-- correspond to none at all. The ambiguous case occurs when the civil time
-- is rolled back, making a calendar day longer than 24 hours. In this case,
-- PostgreSQL silently chooses the second, later possibility. The inconsistent
-- case occurs when the civil time is moved forward, making a calendar day less
-- than 24 hours. In this case, PostgreSQL silently assumes the local time
-- was read off a clock that had not been moved forward at the prescribed time,
-- and moves the clock forward for you. Thus, converting from local time
-- to UTC need not be monotonic, if these inconsistent cases are allowed.
--
-- When retrieving a @timestamptz@, the backend looks at the @time zone@
-- connection variable and then consults the IANA TZ database to calculate
-- an offset for the timestamp in the given time zone.
--
-- Note that while some of the information contained in the IANA TZ database
-- is a bit of a standardized fiction, the conversion from UTC time to a
-- (local time, offset) pair in a particular time zone is always unambiguous,
-- and the result can always be unambiguously converted back to UTC. Thus,
-- hpgsql-simple-compat can interpret such a result as a 'Data.Time.ZonedTime',
-- or use the offset to convert back to 'Data.Time.UTCTime'.
--
-- By contrast, the @timestamp@ type ignores any offsets provided to it,
-- and never sends back an offset. Thus, hpgsql-simple-compat equates this
-- with 'Data.Time.LocalTime', which has no concept of an offset. One can
-- convert between @timestamptz@ and @timestamp@ using the @AT TIME ZONE@
-- operator, whose semantics also demonstrates that @timestamptz@ is
-- 'Data.Time.UTCTime' whereas @timestamp@ is 'Data.Time.LocalTime'.
--
-- PostgreSQL's @timezone@ is a per-connection variable that by default is
-- initialized to @\'localtime\'@, which normally corresponds to the server's
-- time zone. However, this default can be modified on the server side for an
-- entire cluster, or on a per-user or per-database basis. Moreover, a client
-- can modify their instance of the variable at any time, and can apply that
-- change to the remaining duration of the connection, the current transaction,
-- or the execution context of a server-side function. In addition, upon
-- connection initialization, the libpq client checks for the existence of
-- the @PGTZ@ environment variable, and if it exists, modifies @timezone@
-- accordingly.
--
-- With a few caveats, hpgsql-simple-compat is designed so that you can both send
-- and receive timestamps with the server and get a correct result, no matter
-- what the @timezone@ setting is. But it is important to understand the caveats:
--
-- 1. The correctness of server-side computations can depend on the @timezone@
-- setting. Examples include adding an @interval@ to a @timestamptz@, or
-- type casting between @timestamp@ and @timestamptz@, or applying
-- the @DATE@ function to a @timestamptz@.
--
-- 2. The (localtime, offset) pair contained in a 'Data.Time.ZonedTime' result
-- will depend on the @timezone@ setting, although the result will always
-- represent the same instant in time regardless of the time zone.
--
-- 3. Sending a 'Data.Time.LocalTime' and interpreting it as a @timestamptz@
-- can be useful, as it will be converted to UTC via the tz database,
-- but correctness will depend on the @timezone@ setting. You may prefer
-- to use an explicit @AT TIME ZONE@ conversion instead, which would avoid
-- this contextual dependence.
--
-- Furthermore, although these following points don't involve the @timezone@
-- setting, they are related to the last point above:
--
-- 1. Sending a 'Data.Time.UTCTime' and interpreting it as a @timestamp@ can
-- be useful. In practice, the most common context used to disambiguate
-- @timestamp@ is that it represents UTC, and this coding technique will
-- work as expected in this situation.
--
-- 2. Sending a 'Data.Time.ZonedTime' and interpreting it as a @timestamp@ is
-- almost always the wrong thing to do, as the offset will be ignored and
-- discarded. This is likely to lead to inconsistencies in the database,
-- and may lead to partial data loss.
--
-- When dealing with local timestamps that refer to the future, it is often
-- useful to store it as a local time in a @timestamp@ column and store the
-- time zone in a second column. One reason to do this is so that you can
-- convert to UTC on the fly as needed, and be protected against future changes
-- to the TZ database due to changes in local time standards. In any case,
-- 'Data.Time.ZonedTime' is not suitable for this application, because despite
-- its name, it represents an offset and not a time zone. Time zones can change;
-- offsets do not. In reality, we can't convert a local timestamp that occurs
-- sufficiently far in the future to UTC, because we don't know how to do it yet.
--
-- There are a few limitations and caveats that one might need to be aware
-- of with the current implementation when dealing with older timestamps:
--
-- For sufficiently old timestamps in almost all time zones, the IANA TZ
-- database specifies offsets from UTC that is not an integral number of
-- minutes. This corresponds to local mean time; that is, astronomical
-- time in the city that defines the time zone. Different time zones moved
-- away from local mean time to a standard time at different points in
-- history, so \"sufficiently old\" depends on the time zone in question.
--
-- Thus, when retrieving a @timestamptz@ postgresql will in some cases
-- provide seconds in the offset. For example:
--
-- @
-- \$ psql
-- psql (9.4.5)
-- Type \"help\" for help.
--
-- lpsmith=> SET timezone TO \'America/New_York\';
-- SET
-- lpsmith=> VALUES (\'1883-11-18 16:59:59+00\'::timestamptz),
-- (\'1883-11-18 17:00:00+00\'::timestamptz);
-- column1
-- ------------------------------
-- 1883-11-18 12:03:57-04:56:02
-- 1883-11-18 12:00:00-05
-- (2 rows)
-- @
--
-- Both of these timestamps can be parsed as a 'Data.Time.UTCTime' type,
-- however 'Data.Time.ZonedTime' will fail on the former timestamp.
-- Because 'Data.Time.ZonedTime' assumes that offsets are an integer number
-- of minutes, there isn't an particularly good solution here.
--
-- PostgreSQL, like most software, uses the proleptic Gregorian calendar
-- for its date calculations, extending the Gregorian calendar backwards
-- in time before its introduction and pretending that the Julian calendar
-- does not exist. For most purposes, the adoption of the Gregorian calendar
-- ranges from @1582-10-15@ to @1923-03-01@, depending on location and
-- sometimes even political allegiances within a single location.
--
-- Timestamps BCE are not supported. For example, PostgreSQL
-- will emit \"@0045-01-01 BC@\" for the first proleptic Gregorian day of
-- the year the Roman Empire adopted the Julian Calendar, but
-- hpgsql-simple-compat does not (yet?) have the ability to either parse or
-- generate this syntax. Unfortunately this syntax isn't convenient to
-- print or especially parse.
--
-- Also, postgresql itself cannot parse or print dates before @4714-11-24 BC@,
-- which is the Julian date on the proleptic Gregorian Calendar. Although
-- postgresql's timestamp types are perfectly capable of representing timestamps
-- nearly 300,000 years in the past, using this would require hpgsql-simple-compat
-- and other client programs to support binary parameters and results.
--
-- Dealing with years BCE is also complicated slightly by the fact that
-- Haskell's time library has a year \"0000\", which is a convention often
-- used by astronomers, while postgresql adopts the more historically
-- accurate convention that there is no year zero, but rather \"1 BCE\"
-- was immediately followed by \"1 CE\".
module Database.PostgreSQL.Simple.Time
( Unbounded (..),
Date,
UTCTimestamp,
ZonedTimestamp,
LocalTimestamp,
parseDay,
parseUTCTime,
parseZonedTime,
parseLocalTime,
parseTimeOfDay,
parseDate,
parseUTCTimestamp,
parseZonedTimestamp,
parseLocalTimestamp,
parseCalendarDiffTime,
dayToBuilder,
utcTimeToBuilder,
zonedTimeToBuilder,
localTimeToBuilder,
timeOfDayToBuilder,
timeZoneToBuilder,
dateToBuilder,
utcTimestampToBuilder,
zonedTimestampToBuilder,
localTimestampToBuilder,
unboundedToBuilder,
nominalDiffTimeToBuilder,
calendarDiffTimeToBuilder,
)
where
import Database.PostgreSQL.Simple.Time.Implementation
import Hpgsql.Time (Unbounded (..))