Compare commits

..

No commits in common. "e68bc655761b6917644d8cd5190dfc8478207ffa" and "4bf489c554911f66a150043ffb1b199ee5cfbafe" have entirely different histories.

7 changed files with 52 additions and 196 deletions

View File

@ -69,8 +69,7 @@ getScoreByAge conn = do
- [x] Test negative integer decoding, especially for `Integer` - [x] Test negative integer decoding, especially for `Integer`
- [ ] Implement time intervals - [ ] Implement time intervals
- [ ] and zoned time decoding - [ ] and zoned time decoding
- [ ] How about `timezone`? This could prove problematic when the server and application have different time zones - [ ] Implement `fetch` (`fetch_` but with parameter passing)
- [x] Implement `fetch` (`fetch_` but with parameter passing)
- [ ] Implement JSON decoding - [ ] Implement JSON decoding
- [ ] Implement (anonymous) composite types - [ ] Implement (anonymous) composite types
- [ ] Catch [UnicodeException](https://hackage.haskell.org/package/text-2.1/docs/Data-Text-Encoding-Error.html#t:UnicodeException) when decoding text - [ ] Catch [UnicodeException](https://hackage.haskell.org/package/text-2.1/docs/Data-Text-Encoding-Error.html#t:UnicodeException) when decoding text

View File

@ -16,7 +16,6 @@ module Database.PostgreSQL.Opium
, FromField (..) , FromField (..)
, FromRow (..) , FromRow (..)
, RawField (..) , RawField (..)
, fetch
, fetch_ , fetch_
, toListColumnTable , toListColumnTable
) )
@ -46,17 +45,11 @@ import qualified Database.PostgreSQL.LibPQ as LibPQ
import Database.PostgreSQL.Opium.Error (Error (..), ErrorPosition (..)) import Database.PostgreSQL.Opium.Error (Error (..), ErrorPosition (..))
import Database.PostgreSQL.Opium.FromField (FromField (..), fromField, RawField (..)) import Database.PostgreSQL.Opium.FromField (FromField (..), fromField, RawField (..))
import Database.PostgreSQL.Opium.ToParamList (ToParamList (..))
execParams execParams :: Connection -> Text -> ExceptT Error IO Result
:: ToParamList a execParams conn query = do
=> Connection
-> Text
-> a
-> ExceptT Error IO Result
execParams conn query params = do
let queryBytes = Encoding.encodeUtf8 query let queryBytes = Encoding.encodeUtf8 query
liftIO (LibPQ.execParams conn queryBytes (toParamList params) LibPQ.Binary) >>= \case liftIO (LibPQ.execParams conn queryBytes [] LibPQ.Binary) >>= \case
Nothing -> Nothing ->
except $ Left ErrorNoResult except $ Left ErrorNoResult
Just result -> do Just result -> do
@ -67,21 +60,13 @@ execParams conn query params = do
Nothing -> pure result Nothing -> pure result
Just message -> except $ Left $ ErrorInvalidResult status $ Encoding.decodeUtf8 message Just message -> except $ Left $ ErrorInvalidResult status $ Encoding.decodeUtf8 message
fetch fetch_ :: forall a. FromRow a => Connection -> Text -> IO (Either Error [a])
:: forall a b. (ToParamList a, FromRow b) fetch_ conn query = runExceptT $ do
=> Connection result <- execParams conn query
-> Text columnTable <- ExceptT $ getColumnTable @a Proxy result
-> a
-> IO (Either Error [b])
fetch conn query params = runExceptT $ do
result <- execParams conn query params
columnTable <- ExceptT $ getColumnTable @b Proxy result
nRows <- liftIO $ LibPQ.ntuples result nRows <- liftIO $ LibPQ.ntuples result
mapM (ExceptT . fromRow result columnTable) [0..nRows - 1] mapM (ExceptT . fromRow result columnTable) [0..nRows - 1]
fetch_ :: forall a. FromRow a => Connection -> Text -> IO (Either Error [a])
fetch_ conn query = fetch conn query ()
newtype ColumnTable = ColumnTable (Vector (Column, Oid)) newtype ColumnTable = ColumnTable (Vector (Column, Oid))
deriving (Eq, Show) deriving (Eq, Show)

View File

@ -41,9 +41,6 @@ import qualified Database.PostgreSQL.Opium.Oid as Oid
(\/) :: (a -> Bool) -> (a -> Bool) -> a -> Bool (\/) :: (a -> Bool) -> (a -> Bool) -> a -> Bool
p \/ q = \x -> p x || q x p \/ q = \x -> p x || q x
eq :: Eq a => a -> a -> Bool
eq = (==)
fromField :: FromField a => ByteString -> Either String a fromField :: FromField a => ByteString -> Either String a
fromField = fromField =
AP.parseOnly parseField AP.parseOnly parseField
@ -55,13 +52,13 @@ class FromField a where
-- | See https://www.postgresql.org/docs/current/datatype-binary.html. -- | See https://www.postgresql.org/docs/current/datatype-binary.html.
-- Accepts @bytea@. -- Accepts @bytea@.
instance FromField ByteString where instance FromField ByteString where
validOid Proxy = eq Oid.bytea validOid Proxy = Oid.bytea
parseField = AP.takeByteString parseField = AP.takeByteString
-- | See https://www.postgresql.org/docs/current/datatype-character.html. -- | See https://www.postgresql.org/docs/current/datatype-character.html.
-- Accepts @text@, @character@ and @character varying@. -- Accepts @text@, @character@ and @character varying@.
instance FromField Text where instance FromField Text where
validOid Proxy = eq Oid.text \/ eq Oid.character \/ eq Oid.characterVarying validOid Proxy = Oid.text \/ Oid.character \/ Oid.characterVarying
parseField = Encoding.decodeUtf8 <$> AP.takeByteString parseField = Encoding.decodeUtf8 <$> AP.takeByteString
-- Accepts @text@, @character@ and @character varying@. -- Accepts @text@, @character@ and @character varying@.
@ -101,22 +98,22 @@ readWord bs = case BS.length bs of
-- | See https://www.postgresql.org/docs/current/datatype-numeric.html. -- | See https://www.postgresql.org/docs/current/datatype-numeric.html.
-- We assume that 'Int' has 64 bits. This is not guaranteed but reasonable enough. -- We assume that 'Int' has 64 bits. This is not guaranteed but reasonable enough.
instance FromField Int where instance FromField Int where
validOid Proxy = eq Oid.smallint \/ eq Oid.integer \/ eq Oid.bigint validOid Proxy = Oid.smallint \/ Oid.integer \/ Oid.bigint
parseField = readInt =<< AP.takeByteString parseField = readInt =<< AP.takeByteString
-- | See https://www.postgresql.org/docs/current/datatype-numeric.html. -- | See https://www.postgresql.org/docs/current/datatype-numeric.html.
instance FromField Integer where instance FromField Integer where
validOid Proxy = eq Oid.smallint \/ eq Oid.integer \/ eq Oid.bigint validOid Proxy = Oid.smallint \/ Oid.integer \/ Oid.bigint
parseField = readInt =<< AP.takeByteString parseField = readInt =<< AP.takeByteString
instance FromField Word where instance FromField Word where
validOid Proxy = eq Oid.smallint \/ eq Oid.integer \/ eq Oid.bigint validOid Proxy = Oid.smallint \/ Oid.integer \/ Oid.bigint
parseField = readWord =<< AP.takeByteString parseField = readWord =<< AP.takeByteString
-- | See https://www.postgresql.org/docs/current/datatype-numeric.html. -- | See https://www.postgresql.org/docs/current/datatype-numeric.html.
-- Accepts only @real@ fields, not @double precision@. -- Accepts only @real@ fields, not @double precision@.
instance FromField Float where instance FromField Float where
validOid Proxy = eq Oid.real validOid Proxy = Oid.real
-- Afaict there's no cleaner (@base@) way to access the underlying bits. -- Afaict there's no cleaner (@base@) way to access the underlying bits.
-- In C we'd do -- In C we'd do
-- --
@ -128,7 +125,7 @@ instance FromField Float where
-- | See https://www.postgresql.org/docs/current/datatype-numeric.html. -- | See https://www.postgresql.org/docs/current/datatype-numeric.html.
-- Accepts only @double precision@ fields, not @real@. -- Accepts only @double precision@ fields, not @real@.
instance FromField Double where instance FromField Double where
validOid Proxy = eq Oid.doublePrecision validOid Proxy = Oid.doublePrecision
parseField = unsafeCoerce <$> readBigEndian @Word <$> AP.takeByteString parseField = unsafeCoerce <$> readBigEndian @Word <$> AP.takeByteString
boolParser :: Parser Bool boolParser :: Parser Bool
@ -139,7 +136,7 @@ boolParser = AP.choice
-- | See https://www.postgresql.org/docs/current/datatype-boolean.html. -- | See https://www.postgresql.org/docs/current/datatype-boolean.html.
instance FromField Bool where instance FromField Bool where
validOid Proxy = eq Oid.boolean validOid Proxy = Oid.boolean
parseField = boolParser parseField = boolParser
postgresEpoch :: Day postgresEpoch :: Day
@ -154,14 +151,14 @@ fromPostgresJulian x = addDays x postgresEpoch
-- In short, Postgres treats 1 BC as a leap year and doesn't have a year zero. -- 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. -- 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 = eq Oid.date validOid Proxy = Oid.date
parseField = fromPostgresJulian . fromIntegral <$> readBigEndian @Int32 <$> AP.takeByteString parseField = fromPostgresJulian . fromIntegral <$> readBigEndian @Int32 <$> AP.takeByteString
-- | See https://www.postgresql.org/docs/current/datatype-datetime.html. -- | See https://www.postgresql.org/docs/current/datatype-datetime.html.
-- Binary format: https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/adt/date.c;h=ae0f24de2c3c54eb6d0405cdb212597c2407238e;hb=HEAD#l1542. -- Binary format: https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/adt/date.c;h=ae0f24de2c3c54eb6d0405cdb212597c2407238e;hb=HEAD#l1542.
-- Accepts @time@. -- Accepts @time@.
instance FromField DiffTime where instance FromField DiffTime where
validOid Proxy = eq Oid.time validOid Proxy = Oid.time
parseField = microsecondsToDiffTime . fromIntegral <$> readBigEndian @Int <$> AP.takeByteString parseField = microsecondsToDiffTime . fromIntegral <$> readBigEndian @Int <$> AP.takeByteString
where where
microsecondsToDiffTime :: Integer -> DiffTime microsecondsToDiffTime :: Integer -> DiffTime
@ -171,7 +168,7 @@ instance FromField DiffTime where
-- Binary format: https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/adt/date.c;h=ae0f24de2c3c54eb6d0405cdb212597c2407238e;hb=HEAD#l1542. -- Binary format: https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/adt/date.c;h=ae0f24de2c3c54eb6d0405cdb212597c2407238e;hb=HEAD#l1542.
-- Accepts @time@. -- Accepts @time@.
instance FromField TimeOfDay where instance FromField TimeOfDay where
validOid Proxy = eq Oid.time validOid Proxy = Oid.time
parseField = timeToTimeOfDay <$> parseField @DiffTime parseField = timeToTimeOfDay <$> parseField @DiffTime
fromPostgresTimestamp :: Int -> (Day, DiffTime) fromPostgresTimestamp :: Int -> (Day, DiffTime)
@ -184,7 +181,7 @@ fromPostgresTimestamp ts = (day, time)
-- | See https://www.postgresql.org/docs/current/datatype-datetime.html. -- | See https://www.postgresql.org/docs/current/datatype-datetime.html.
-- Accepts @timestamp with timezone@. -- Accepts @timestamp with timezone@.
instance FromField UTCTime where instance FromField UTCTime where
validOid Proxy = eq Oid.timestampWithTimezone validOid Proxy = Oid.timestampWithTimezone
parseField = toUTCTime . fromPostgresTimestamp <$> readBigEndian @Int <$> AP.takeByteString parseField = toUTCTime . fromPostgresTimestamp <$> readBigEndian @Int <$> AP.takeByteString
where where
toUTCTime (day, time) = UTCTime day time toUTCTime (day, time) = UTCTime day time

View File

@ -2,62 +2,65 @@ module Database.PostgreSQL.Opium.Oid where
import Database.PostgreSQL.LibPQ (Oid (..)) import Database.PostgreSQL.LibPQ (Oid (..))
eq :: Eq a => a -> a -> Bool
eq = (==)
-- raw byte string -- raw byte string
bytea :: Oid bytea :: Oid -> Bool
bytea = Oid 17 bytea = eq $ Oid 17
-- string types -- string types
text :: Oid text :: Oid -> Bool
text = Oid 25 text = eq $ Oid 25
character :: Oid character :: Oid -> Bool
character = Oid 1042 character = eq $ Oid 1042
characterVarying :: Oid characterVarying :: Oid -> Bool
characterVarying = Oid 1043 characterVarying = eq $ Oid 1043
-- integer types -- integer types
-- | 16-bit integer -- | 16-bit integer
smallint :: Oid smallint :: Oid -> Bool
smallint = Oid 21 smallint = eq $ Oid 21
-- | 32-bit integer -- | 32-bit integer
integer :: Oid integer :: Oid -> Bool
integer = Oid 23 integer = eq $ Oid 23
-- | 64-bit integer -- | 64-bit integer
bigint :: Oid bigint :: Oid -> Bool
bigint = Oid 20 bigint = eq $ Oid 20
-- floating point types -- floating point types
-- | 32-bit IEEE float -- | 32-bit IEEE float
real :: Oid real :: Oid -> Bool
real = Oid 700 real = eq $ Oid 700
-- | 64-bit IEEE float -- | 64-bit IEEE float
doublePrecision :: Oid doublePrecision :: Oid -> Bool
doublePrecision = Oid 701 doublePrecision = eq $ Oid 701
-- | Oid -- | Boolean
boolean :: Oid boolean :: Oid -> Bool
boolean = Oid 16 boolean = eq $ Oid 16
-- | Single days/dates. -- | Single days/dates.
date :: Oid date :: Oid -> Bool
date = Oid 1082 date = eq $ Oid 1082
-- | Time of day. -- | Time of day.
time :: Oid time :: Oid -> Bool
time = Oid 1083 time = eq $ Oid 1083
-- | A point in time. -- | A point in time.
timestamp :: Oid timestamp :: Oid -> Bool
timestamp = Oid 1114 timestamp = eq $ Oid 1114
-- | A point in time. -- | A point in time.
timestampWithTimezone :: Oid timestampWithTimezone :: Oid -> Bool
timestampWithTimezone = Oid 1184 timestampWithTimezone = eq $ Oid 1184

View File

@ -1,50 +0,0 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeApplications #-}
module Database.PostgreSQL.Opium.ToField
( ToField (..)
) where
import Data.Bits (Bits (..))
import Data.ByteString (ByteString)
import Data.List (singleton)
import Data.Text (Text)
import Data.Word (Word32)
import Database.PostgreSQL.LibPQ (Format (..), Oid)
import Unsafe.Coerce (unsafeCoerce)
import qualified Data.ByteString as BS
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Encoding
import qualified Database.PostgreSQL.Opium.Oid as Oid
class ToField a where
toField :: a -> Maybe (Oid, ByteString, Format)
instance ToField ByteString where
toField x = Just (Oid.bytea, x, Binary)
instance ToField Text where
toField x = Just (Oid.text, Encoding.encodeUtf8 x, Binary)
instance ToField String where
toField = toField . Text.pack
instance ToField Char where
toField = toField . singleton
-- Potentially slow, but good enough for now
encodeBigEndian :: (Integral a, Bits a) => Int -> a -> ByteString
encodeBigEndian n = BS.pack . go [] n
where
go acc 0 _ = acc
go acc i x = go (fromIntegral (x .&. 0xff) : acc) (i - 1) (x `shiftR` 8)
instance ToField Int where
toField x = Just (Oid.bigint, encodeBigEndian 8 x, Binary)
instance ToField Float where
toField x = Just (Oid.real, encodeBigEndian @Word32 4 $ unsafeCoerce x, Binary)
instance ToField Double where
toField x = Just (Oid.doublePrecision, encodeBigEndian @Word 8 $ unsafeCoerce x, Binary)

View File

@ -1,62 +0,0 @@
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeOperators #-}
module Database.PostgreSQL.Opium.ToParamList
( ToParamList (..)
) where
import Data.ByteString (ByteString)
import Data.Functor.Identity (Identity)
import Database.PostgreSQL.LibPQ (Format, Oid)
import GHC.Generics (Generic, K1 (..), M1 (..), Rec0, Rep, U1 (..), from, (:*:) (..))
import Database.PostgreSQL.Opium.ToField (ToField (..))
class ToParamList a where
toParamList :: a -> [Maybe (Oid, ByteString, Format)]
default toParamList :: (Generic a, ToParamList' (Rep a)) => a -> [Maybe (Oid, ByteString, Format)]
toParamList = toParamList' . from
instance ToField a => ToParamList [a] where
toParamList = map toField
instance ToParamList () where
instance ToField a => ToParamList (Identity a) where
instance (ToField a, ToField b) => ToParamList (a, b) where
instance (ToField a, ToField b, ToField c) => ToParamList (a, b, c) where
instance (ToField a, ToField b, ToField c, ToField d) => ToParamList (a, b, c, d) where
instance (ToField a, ToField b, ToField c, ToField d, ToField e) => ToParamList (a, b, c, d, e) where
instance (ToField a, ToField b, ToField c, ToField d, ToField e, ToField f) => ToParamList (a, b, c, d, e, f) where
instance (ToField a, ToField b, ToField c, ToField d, ToField e, ToField f, ToField g) => ToParamList (a, b, c, d, e, f, g) where
instance (ToField a, ToField b, ToField c, ToField d, ToField e, ToField f, ToField g, ToField h) => ToParamList (a, b, c, d, e, f, g, h) where
instance (ToField a, ToField b, ToField c, ToField d, ToField e, ToField f, ToField g, ToField h, ToField i) => ToParamList (a, b, c, d, e, f, g, h, i) where
instance (ToField a, ToField b, ToField c, ToField d, ToField e, ToField f, ToField g, ToField h, ToField i, ToField j) => ToParamList (a, b, c, d, e, f, g, h, i, j) where
instance (ToField a, ToField b, ToField c, ToField d, ToField e, ToField f, ToField g, ToField h, ToField i, ToField j, ToField k) => ToParamList (a, b, c, d, e, f, g, h, i, j, k) where
class ToParamList' f where
toParamList' :: f p -> [Maybe (Oid, ByteString, Format)]
instance ToField t => ToParamList' (Rec0 t) where
toParamList' (K1 x) = [toField x]
instance ToParamList' f => ToParamList' (M1 t c f) where
toParamList' (M1 x) = toParamList' x
instance ToParamList' U1 where
toParamList' U1 = []
instance (ToParamList' f, ToParamList' g) => ToParamList' (f :*: g) where
toParamList' (x :*: y) = toParamList' x ++ toParamList' y

View File

@ -7,7 +7,6 @@
module Database.PostgreSQL.OpiumSpec (spec) where module Database.PostgreSQL.OpiumSpec (spec) where
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import Data.Functor.Identity (Identity (..))
import Data.Proxy (Proxy (..)) import Data.Proxy (Proxy (..))
import Data.Text (Text) import Data.Text (Text)
import Database.PostgreSQL.LibPQ (Connection) import Database.PostgreSQL.LibPQ (Connection)
@ -47,12 +46,6 @@ data ScoreByAge = ScoreByAge
instance Opium.FromRow ScoreByAge where instance Opium.FromRow ScoreByAge where
data Only a = Only
{ only :: a
} deriving (Eq, Generic, Show)
instance Opium.FromField a => Opium.FromRow (Only a) where
isLeft :: Either a b -> Bool isLeft :: Either a b -> Bool
isLeft (Left _) = True isLeft (Left _) = True
isLeft _ = False isLeft _ = False
@ -124,15 +117,6 @@ spec = do
row <- Opium.fromRow result columnTable 0 row <- Opium.fromRow result columnTable 0
row `shouldBe` Right (ManyFields "abc" 42 1.0 "test" True) row `shouldBe` Right (ManyFields "abc" 42 1.0 "test" True)
describe "fetch" $ do
it "Passes numbered parameters and retrieves a list of rows" $ \conn -> do
rows <- Opium.fetch conn "SELECT ($1 + $2) AS only" (17 :: Int, 25 :: Int)
rows `shouldBe` Right [Only (42 :: Int)]
it "Uses Identity to pass single parameters" $ \conn -> do
rows <- Opium.fetch conn "SELECT count(*) AS only FROM person WHERE name = $1" $ Identity ("paul" :: Text)
rows `shouldBe` Right [Only (1 :: Int)]
describe "fetch_" $ do describe "fetch_" $ do
it "Retrieves a list of rows" $ \conn -> do it "Retrieves a list of rows" $ \conn -> do
rows <- Opium.fetch_ conn "SELECT * FROM person" rows <- Opium.fetch_ conn "SELECT * FROM person"