diff --git a/README.md b/README.md index bfeb0f7..35bd102 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ getScoreByAge conn = do - [x] Implement `Float` and `Double` decoding - [x] Clean up and document column table stuff - [x] Decode `LibPQ.Binary` +- [x] Implement `date -> Day` decoding - [ ] Implement `fetch` (`fetch_` but with parameter passing) - [ ] Implement `UTCTime` and zoned time decoding - [ ] Implement JSON decoding diff --git a/flake.nix b/flake.nix index aa14d9b..3f94d38 100644 --- a/flake.nix +++ b/flake.nix @@ -20,6 +20,7 @@ hspec postgresql-libpq text + time transformers vector ])) diff --git a/lib/Database/PostgreSQL/Opium/FromField.hs b/lib/Database/PostgreSQL/Opium/FromField.hs index 9acdfc1..61cf8d8 100644 --- a/lib/Database/PostgreSQL/Opium/FromField.hs +++ b/lib/Database/PostgreSQL/Opium/FromField.hs @@ -11,7 +11,9 @@ import Data.Attoparsec.ByteString (Parser) import Data.Bits (Bits (..)) import Data.ByteString (ByteString) import Data.Functor (($>)) +import Data.Int (Int32) import Data.Proxy (Proxy (..)) +import Data.Time (Day (..)) import Data.Text (Text) import Data.Word (Word32, Word64) import Database.PostgreSQL.LibPQ (Oid) @@ -107,3 +109,11 @@ boolParser = AP.choice instance FromField Bool where validOid Proxy = Oid.boolean parseField = boolParser + +-- | 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. +instance FromField Day where + validOid Proxy = Oid.date + parseField = fromJulianDay . fromIntegral <$> intParser @Int32 + where + fromJulianDay x = ModifiedJulianDay $ x + 51544 diff --git a/lib/Database/PostgreSQL/Opium/Oid.hs b/lib/Database/PostgreSQL/Opium/Oid.hs index 014b25b..88ea207 100644 --- a/lib/Database/PostgreSQL/Opium/Oid.hs +++ b/lib/Database/PostgreSQL/Opium/Oid.hs @@ -48,3 +48,7 @@ doublePrecision = eq $ Oid 701 -- | Boolean boolean :: Oid -> Bool boolean = eq $ Oid 16 + +-- | Single days/dates. +date :: Oid -> Bool +date = eq $ Oid 1082 diff --git a/opium.cabal b/opium.cabal index 44a17bd..7ff831e 100644 --- a/opium.cabal +++ b/opium.cabal @@ -79,6 +79,7 @@ library containers, postgresql-libpq, text, + time, transformers, vector @@ -122,4 +123,5 @@ test-suite opium-test bytestring, hspec, postgresql-libpq, + time, text diff --git a/test/Database/PostgreSQL/Opium/FromFieldSpec.hs b/test/Database/PostgreSQL/Opium/FromFieldSpec.hs index 9ccdf95..d0cefad 100644 --- a/test/Database/PostgreSQL/Opium/FromFieldSpec.hs +++ b/test/Database/PostgreSQL/Opium/FromFieldSpec.hs @@ -4,6 +4,7 @@ module Database.PostgreSQL.Opium.FromFieldSpec (spec) where import Data.ByteString (ByteString) +import Data.Time (Day (..), fromGregorian) import Data.Text (Text) import Database.PostgreSQL.LibPQ (Connection) import Database.PostgreSQL.Opium (FromRow) @@ -72,6 +73,12 @@ newtype ABool = ABool instance FromRow ABool where +newtype ADay = ADay + { day :: Day + } deriving (Eq, Generic, Show) + +instance FromRow ADay where + shouldFetch :: (Eq a, FromRow a, Show a) => Connection -> ByteString -> [a] -> IO () shouldFetch conn query expectedRows = do actualRows <- Opium.fetch_ conn query @@ -200,3 +207,10 @@ spec = do shouldFetch conn "SELECT 'no'::boolean AS bool" [ABool False] shouldFetch conn "SELECT 'off'::boolean AS bool" [ABool False] shouldFetch conn "SELECT 0::boolean AS bool" [ABool False] + + describe "FromField Day" $ do + it "Decodes date" $ \conn -> do + shouldFetch conn "SELECT date '1970-01-01' AS day" [ADay $ fromGregorian 1970 1 1] + shouldFetch conn "SELECT date '2023-09-23' AS day" [ADay $ fromGregorian 2023 9 23] + -- Example from postgres doc page + shouldFetch conn "SELECT date 'J2451187' AS day" [ADay $ fromGregorian 1999 1 8]