Add comment about difference between Postgres and Haskell dates
This commit is contained in:
parent
9c93d3a42b
commit
4f39966da2
@ -19,13 +19,12 @@ import Data.Proxy (Proxy (..))
|
|||||||
import Data.Time
|
import Data.Time
|
||||||
( Day (..)
|
( Day (..)
|
||||||
, DiffTime
|
, DiffTime
|
||||||
|
, LocalTime (..)
|
||||||
, TimeOfDay
|
, TimeOfDay
|
||||||
, UTCTime (..)
|
|
||||||
, addDays
|
, addDays
|
||||||
, fromGregorian
|
, fromGregorian
|
||||||
, picosecondsToDiffTime
|
, picosecondsToDiffTime
|
||||||
, timeToTimeOfDay
|
, timeToTimeOfDay
|
||||||
, toGregorian
|
|
||||||
)
|
)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Word (Word32, Word64)
|
import Data.Word (Word32, Word64)
|
||||||
@ -131,15 +130,13 @@ postgresEpoch :: Day
|
|||||||
postgresEpoch = fromGregorian 2000 1 1
|
postgresEpoch = fromGregorian 2000 1 1
|
||||||
|
|
||||||
fromPostgresJulian :: Integer -> Day
|
fromPostgresJulian :: Integer -> Day
|
||||||
fromPostgresJulian x
|
fromPostgresJulian x = addDays x postgresEpoch
|
||||||
| year <= 0 = fromGregorian (year - 1) month dayOfMonth
|
|
||||||
| otherwise = day
|
|
||||||
where
|
|
||||||
day = addDays (fromIntegral x) postgresEpoch
|
|
||||||
(year, month, dayOfMonth) = toGregorian day
|
|
||||||
|
|
||||||
-- | See https://www.postgresql.org/docs/current/datatype-datetime.html.
|
-- | See https://www.postgresql.org/docs/current/datatype-datetime.html.
|
||||||
-- Relevant as well: https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/adt/datetime.c;h=267dfd37b2e8b9bc63797c69b9ca2e45e6bfde61;hb=HEAD#l267.
|
-- Relevant as well: https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/adt/datetime.c;h=267dfd37b2e8b9bc63797c69b9ca2e45e6bfde61;hb=HEAD#l267.
|
||||||
|
-- Note that Postgres uses the proleptic Gregorian calendar, whereas @Show Day@ and @fromGregorian@ use an astronomical calendar.
|
||||||
|
-- In short, Postgres treats 1 BC as a leap year and doesn't have a year zero.
|
||||||
|
-- This means that working with negative dates will be different in Postgres and your application code.
|
||||||
instance FromField Day where
|
instance FromField Day where
|
||||||
validOid Proxy = Oid.date
|
validOid Proxy = Oid.date
|
||||||
parseField = fromPostgresJulian . fromIntegral <$> intParser @Int32
|
parseField = fromPostgresJulian . fromIntegral <$> intParser @Int32
|
||||||
@ -161,18 +158,23 @@ instance FromField TimeOfDay where
|
|||||||
validOid Proxy = Oid.time
|
validOid Proxy = Oid.time
|
||||||
parseField = timeToTimeOfDay <$> parseField @DiffTime
|
parseField = timeToTimeOfDay <$> parseField @DiffTime
|
||||||
|
|
||||||
instance FromField UTCTime where
|
-- | See https://www.postgresql.org/docs/current/datatype-datetime.html.
|
||||||
|
-- | Accepts @timestamp@.
|
||||||
|
-- Note that Postgres uses the proleptic Gregorian calendar, whereas @Show Day@ and @fromGregorian@ use an astronomical calendar.
|
||||||
|
-- In short, Postgres treats 1 BC as a leap year and doesn't have a year zero.
|
||||||
|
-- This means that working with negative dates will be different in Postgres and your application code.
|
||||||
|
instance FromField LocalTime where
|
||||||
validOid Proxy = Oid.timestamp
|
validOid Proxy = Oid.timestamp
|
||||||
parseField = fromPostgresTimestamp <$> intParser @Int
|
parseField = fromPostgresTimestamp <$> intParser @Int
|
||||||
where
|
where
|
||||||
fromPostgresTimestamp :: Int -> UTCTime
|
fromPostgresTimestamp :: Int -> LocalTime
|
||||||
fromPostgresTimestamp ts =
|
fromPostgresTimestamp ts =
|
||||||
let
|
let
|
||||||
(days, microseconds) = ts `divMod` (86400 * 1000000)
|
(days, microseconds) = ts `divMod` (86400 * 1000000)
|
||||||
day = fromPostgresJulian $ fromIntegral days
|
day = fromPostgresJulian $ fromIntegral days
|
||||||
time = picosecondsToDiffTime $ fromIntegral microseconds * 1000000
|
time = picosecondsToDiffTime $ fromIntegral microseconds * 1000000
|
||||||
in
|
in
|
||||||
UTCTime day time
|
LocalTime day (timeToTimeOfDay time)
|
||||||
|
|
||||||
newtype RawField a = RawField a
|
newtype RawField a = RawField a
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
@ -7,11 +7,10 @@ import Data.ByteString (ByteString)
|
|||||||
import Data.Time
|
import Data.Time
|
||||||
( Day (..)
|
( Day (..)
|
||||||
, DiffTime
|
, DiffTime
|
||||||
|
, LocalTime (..)
|
||||||
, TimeOfDay (..)
|
, TimeOfDay (..)
|
||||||
, UTCTime (..)
|
|
||||||
, fromGregorian
|
, fromGregorian
|
||||||
, secondsToDiffTime
|
, secondsToDiffTime
|
||||||
, timeOfDayToTime
|
|
||||||
)
|
)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Database.PostgreSQL.LibPQ (Connection)
|
import Database.PostgreSQL.LibPQ (Connection)
|
||||||
@ -101,11 +100,11 @@ newtype ATimeOfDay = ATimeOfDay
|
|||||||
|
|
||||||
instance FromRow ATimeOfDay where
|
instance FromRow ATimeOfDay where
|
||||||
|
|
||||||
newtype AUTCTime = AUTCTime
|
newtype ALocalTime = ALocalTime
|
||||||
{ utctime :: UTCTime
|
{ localtime :: LocalTime
|
||||||
} deriving (Eq, Generic, Show)
|
} deriving (Eq, Generic, Show)
|
||||||
|
|
||||||
instance FromRow AUTCTime where
|
instance FromRow ALocalTime where
|
||||||
|
|
||||||
newtype ARawField = ARawField
|
newtype ARawField = ARawField
|
||||||
{ raw :: Opium.RawField ByteString
|
{ raw :: Opium.RawField ByteString
|
||||||
@ -252,7 +251,7 @@ spec = do
|
|||||||
-- BC
|
-- BC
|
||||||
-- See https://www.postgresql.org/docs/current/datetime-input-rules.html:
|
-- See https://www.postgresql.org/docs/current/datetime-input-rules.html:
|
||||||
-- "If BC has been specified, negate the year and add one for internal storage. (There is no year zero in the Gregorian calendar, so numerically 1 BC becomes year zero.)"
|
-- "If BC has been specified, negate the year and add one for internal storage. (There is no year zero in the Gregorian calendar, so numerically 1 BC becomes year zero.)"
|
||||||
shouldFetch conn "SELECT date 'January 1, 4710 BC' AS day" [ADay $ fromGregorian (-4710) 1 1]
|
shouldFetch conn "SELECT date '0001-02-29 BC' AS day" [ADay $ fromGregorian 0 2 29]
|
||||||
|
|
||||||
describe "FromField DiffTime" $ do
|
describe "FromField DiffTime" $ do
|
||||||
it "Decodes the time" $ \conn -> do
|
it "Decodes the time" $ \conn -> do
|
||||||
@ -266,19 +265,20 @@ spec = do
|
|||||||
shouldFetch conn "SELECT time '00:01:00' AS timeofday" [ATimeOfDay $ TimeOfDay 0 1 0]
|
shouldFetch conn "SELECT time '00:01:00' AS timeofday" [ATimeOfDay $ TimeOfDay 0 1 0]
|
||||||
shouldFetch conn "SELECT time '13:07:43' AS timeofday" [ATimeOfDay $ TimeOfDay 13 7 43]
|
shouldFetch conn "SELECT time '13:07:43' AS timeofday" [ATimeOfDay $ TimeOfDay 13 7 43]
|
||||||
|
|
||||||
describe "FromField UTCTime" $ do
|
describe "FromField LocalTime" $ do
|
||||||
it "Decodes timestamp" $ \conn -> do
|
it "Decodes timestamp" $ \conn -> do
|
||||||
let ts0 = UTCTime (fromGregorian 2023 10 2) (timeOfDayToTime $ TimeOfDay 12 42 23)
|
let ts0 = LocalTime (fromGregorian 2023 10 2) (TimeOfDay 12 42 23)
|
||||||
shouldFetch conn "SELECT timestamp '2023-10-02 12:42:23' AS utctime" [AUTCTime ts0]
|
shouldFetch conn "SELECT timestamp '2023-10-02 12:42:23' AS localtime" [ALocalTime ts0]
|
||||||
|
|
||||||
let ts1 = UTCTime (fromGregorian 294275 12 31) (timeOfDayToTime $ TimeOfDay 23 59 59)
|
let ts1 = LocalTime (fromGregorian 294275 12 31) (TimeOfDay 23 59 59)
|
||||||
shouldFetch conn "SELECT timestamp '294275-12-31 23:59:59' AS utctime" [AUTCTime ts1]
|
shouldFetch conn "SELECT timestamp '294275-12-31 23:59:59' AS localtime" [ALocalTime ts1]
|
||||||
|
|
||||||
let ts2 = UTCTime (fromGregorian 1 1 1) (timeOfDayToTime $ TimeOfDay 0 0 0)
|
let ts2 = LocalTime (fromGregorian 1 1 1) (TimeOfDay 0 0 0)
|
||||||
shouldFetch conn "SELECT timestamp '0001-01-01 00:00:00' AS utctime" [AUTCTime ts2]
|
shouldFetch conn "SELECT timestamp '0001-01-01 00:00:00' AS localtime" [ALocalTime ts2]
|
||||||
|
|
||||||
let ts3 = UTCTime (fromGregorian (-4710) 1 1) (timeOfDayToTime $ TimeOfDay 0 0 0)
|
-- See note at the FromField Day instance.
|
||||||
shouldFetch conn "SELECT timestamp 'January 1, 4710 BC 00:00:00' AS utctime" [AUTCTime ts3]
|
let ts3 = LocalTime (fromGregorian 0 2 29) (TimeOfDay 0 0 0)
|
||||||
|
shouldFetch conn "SELECT timestamp '0001-02-29 BC 00:00:00' AS localtime" [ALocalTime ts3]
|
||||||
|
|
||||||
describe "FromField RawField" $ do
|
describe "FromField RawField" $ do
|
||||||
it "Simply returns the bytestring without decoding it" $ \conn -> do
|
it "Simply returns the bytestring without decoding it" $ \conn -> do
|
||||||
|
Loading…
x
Reference in New Issue
Block a user