From 4bf489c554911f66a150043ffb1b199ee5cfbafe Mon Sep 17 00:00:00 2001 From: Paul Brinkmeier Date: Mon, 8 Jan 2024 15:28:14 +0100 Subject: [PATCH] Implement UTCTime decoding Remove LocalTime decoding because it requires IO to convert from Postgres time. --- README.md | 1 + lib/Database/PostgreSQL/Opium/FromField.hs | 29 +++++++++---------- lib/Database/PostgreSQL/Opium/Oid.hs | 4 +++ .../PostgreSQL/Opium/FromFieldSpec.hs | 29 ++++++++++--------- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 14680f2..47b0043 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ getScoreByAge conn = do - [x] Implement `UTCTime` - [x] Implement `ByteString` decoding (`bytea`) - [x] Test negative integer decoding, especially for `Integer` +- [ ] Implement time intervals - [ ] and zoned time decoding - [ ] Implement `fetch` (`fetch_` but with parameter passing) - [ ] Implement JSON decoding diff --git a/lib/Database/PostgreSQL/Opium/FromField.hs b/lib/Database/PostgreSQL/Opium/FromField.hs index b2f14c6..30b21c9 100644 --- a/lib/Database/PostgreSQL/Opium/FromField.hs +++ b/lib/Database/PostgreSQL/Opium/FromField.hs @@ -19,8 +19,8 @@ import Data.Proxy (Proxy (..)) import Data.Time ( Day (..) , DiffTime - , LocalTime (..) , TimeOfDay + , UTCTime (..) , addDays , fromGregorian , picosecondsToDiffTime @@ -171,23 +171,20 @@ instance FromField TimeOfDay where validOid Proxy = Oid.time parseField = timeToTimeOfDay <$> parseField @DiffTime +fromPostgresTimestamp :: Int -> (Day, DiffTime) +fromPostgresTimestamp ts = (day, time) + where + (days, microseconds) = ts `divMod` (86400 * 1000000) + day = fromPostgresJulian $ fromIntegral days + time = picosecondsToDiffTime $ fromIntegral microseconds * 1000000 + -- | 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 - parseField = fromPostgresTimestamp <$> readBigEndian @Int <$> AP.takeByteString +-- Accepts @timestamp with timezone@. +instance FromField UTCTime where + validOid Proxy = Oid.timestampWithTimezone + parseField = toUTCTime . fromPostgresTimestamp <$> readBigEndian @Int <$> AP.takeByteString where - fromPostgresTimestamp :: Int -> LocalTime - fromPostgresTimestamp ts = - let - (days, microseconds) = ts `divMod` (86400 * 1000000) - day = fromPostgresJulian $ fromIntegral days - time = picosecondsToDiffTime $ fromIntegral microseconds * 1000000 - in - LocalTime day (timeToTimeOfDay time) + toUTCTime (day, time) = UTCTime day time newtype RawField a = RawField a deriving (Eq, Show) diff --git a/lib/Database/PostgreSQL/Opium/Oid.hs b/lib/Database/PostgreSQL/Opium/Oid.hs index e81205a..83399ec 100644 --- a/lib/Database/PostgreSQL/Opium/Oid.hs +++ b/lib/Database/PostgreSQL/Opium/Oid.hs @@ -60,3 +60,7 @@ time = eq $ Oid 1083 -- | A point in time. timestamp :: Oid -> Bool timestamp = eq $ Oid 1114 + +-- | A point in time. +timestampWithTimezone :: Oid -> Bool +timestampWithTimezone = eq $ Oid 1184 diff --git a/test/Database/PostgreSQL/Opium/FromFieldSpec.hs b/test/Database/PostgreSQL/Opium/FromFieldSpec.hs index a0f9992..878979b 100644 --- a/test/Database/PostgreSQL/Opium/FromFieldSpec.hs +++ b/test/Database/PostgreSQL/Opium/FromFieldSpec.hs @@ -7,10 +7,11 @@ import Data.ByteString (ByteString) import Data.Time ( Day (..) , DiffTime - , LocalTime (..) , TimeOfDay (..) + , UTCTime (..) , fromGregorian , secondsToDiffTime + , timeOfDayToTime ) import Data.Text (Text) import Database.PostgreSQL.LibPQ (Connection) @@ -100,11 +101,11 @@ newtype ATimeOfDay = ATimeOfDay instance FromRow ATimeOfDay where -newtype ALocalTime = ALocalTime - { localtime :: LocalTime +newtype AUTCTime = AUTCTime + { utctime :: UTCTime } deriving (Eq, Generic, Show) -instance FromRow ALocalTime where +instance FromRow AUTCTime where newtype ARawField = ARawField { raw :: Opium.RawField ByteString @@ -283,20 +284,20 @@ spec = do 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] - describe "FromField LocalTime" $ do - it "Decodes timestamp" $ \conn -> do - let ts0 = LocalTime (fromGregorian 2023 10 2) (TimeOfDay 12 42 23) - shouldFetch conn "SELECT timestamp '2023-10-02 12:42:23' AS localtime" [ALocalTime ts0] + describe "FromField UTCTime" $ do + it "Decodes timestamp with timezone" $ \conn -> do + let ts0 = UTCTime (fromGregorian 2023 10 2) (timeOfDayToTime $ TimeOfDay 12 42 23) + shouldFetch conn "SELECT timestamp with time zone '2023-10-02 12:42:23' AS utctime" [AUTCTime ts0] - let ts1 = LocalTime (fromGregorian 294275 12 31) (TimeOfDay 23 59 59) - shouldFetch conn "SELECT timestamp '294275-12-31 23:59:59' AS localtime" [ALocalTime ts1] + let ts1 = UTCTime (fromGregorian 294275 12 31) (timeOfDayToTime $ TimeOfDay 23 59 59) + shouldFetch conn "SELECT timestamp with time zone '294275-12-31 23:59:59' AS utctime" [AUTCTime ts1] - let ts2 = LocalTime (fromGregorian 1 1 1) (TimeOfDay 0 0 0) - shouldFetch conn "SELECT timestamp '0001-01-01 00:00:00' AS localtime" [ALocalTime ts2] + let ts2 = UTCTime (fromGregorian 1 1 1) (timeOfDayToTime $ TimeOfDay 0 0 0) + shouldFetch conn "SELECT timestamp with time zone '0001-01-01 00:00:00' AS utctime" [AUTCTime ts2] -- See note at the FromField Day instance. - 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] + let ts3 = UTCTime (fromGregorian 0 2 29) (timeOfDayToTime $ TimeOfDay 0 0 0) + shouldFetch conn "SELECT timestamp with time zone '0001-02-29 BC 00:00:00' AS utctime" [AUTCTime ts3] describe "FromField RawField" $ do it "Simply returns the bytestring without decoding it" $ \conn -> do