preql 0.5 → 0.6
raw patch · 17 files changed
+466/−141 lines, 17 filesdep +unordered-containersdep ~basedep ~generic-random
Dependencies added: unordered-containers
Dependency ranges changed: base, generic-random
Files
- CHANGELOG.md +6/−0
- benchmark/Bench.hs +4/−3
- preql.cabal +11/−8
- src/Preql.hs +3/−1
- src/Preql/Effect.hs +0/−1
- src/Preql/Effect/Internal.hs +0/−1
- src/Preql/FromSql.hs +1/−1
- src/Preql/FromSql/Class.hs +9/−2
- src/Preql/FromSql/Instances.hs +135/−41
- src/Preql/FromSql/TH.hs +38/−0
- src/Preql/FromSql/Tuple.hs +67/−0
- src/Preql/Wire.hs +3/−3
- src/Preql/Wire/Decode.hs +0/−42
- src/Preql/Wire/Errors.hs +1/−1
- src/Preql/Wire/Query.hs +106/−25
- test/Test.hs +4/−4
- test/Test/Wire.hs +78/−8
CHANGELOG.md view
@@ -1,3 +1,9 @@+# 0.6 (2021-07-31)++- decode array types+- decode composite types with `Tuple` newtype+- cache OID lookups+ # 0.5 (2021-01-10) - fix `deriveFromSql` which was completely unusable
benchmark/Bench.hs view
@@ -7,6 +7,7 @@ module Main where import Preql+import Preql.Wire.Query (Connection(..), connectdbSharedCache) import qualified Preql.Wire.TypeInfo.Static as OID import Control.DeepSeq (NFData(..))@@ -39,10 +40,10 @@ , PQ.Oid, PQ.Oid, PQ.Oid)) ] -connectDB :: IO PQ.Connection+connectDB :: IO Connection connectDB = do- conn <- PQ.connectdb =<< connectionString- status <- PQ.status conn+ conn@(Connection rawConn _) <- connectdbSharedCache =<< connectionString+ status <- PQ.status rawConn unless (status == PQ.ConnectionOk) (error "bad connection") return conn
preql.cabal view
@@ -1,13 +1,13 @@ cabal-version: 1.18 --- This file has been generated from package.yaml by hpack version 0.34.3.+-- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack ----- hash: d56204964cb9b2be1a806f9feb77bc975fc037b053f1edf3b775c7dec9d2fed1+-- hash: f4dbe72a66bc70347b232733521f1dc19162d6cae9eb9467c4f262693e91d5b2 name: preql-version: 0.5+version: 0.6 synopsis: safe PostgreSQL queries using Quasiquoters description: Before you Post(gres)QL, preql. .@@ -48,6 +48,7 @@ Preql.FromSql.Class Preql.FromSql.Instances Preql.FromSql.TH+ Preql.FromSql.Tuple Preql.Imports Preql.QuasiQuoter.Common Preql.QuasiQuoter.Raw.Lex@@ -60,7 +61,6 @@ Preql.QuasiQuoter.Syntax.Syntax Preql.QuasiQuoter.Syntax.TH Preql.Wire- Preql.Wire.Decode Preql.Wire.Errors Preql.Wire.Internal Preql.Wire.Orphans@@ -81,7 +81,7 @@ build-depends: aeson >=1.4.7.1 , array >=0.5.4.0- , base >=4.13.0.0 && <4.15+ , base >=4.13.0.0 && <5.0 , binary-parser >=0.5.6 , bytestring >=0.10.10.0 , bytestring-strict-builder >=0.4.5.3@@ -96,6 +96,7 @@ , th-lift-instances >=0.1.17 , time >=1.9.3 , transformers >=0.5.6.2+ , unordered-containers , uuid >=1.3.13 , vector >=0.12.1.2 , vector-sized >=1.4.1@@ -121,13 +122,13 @@ build-depends: aeson >=1.4.7.1 , array >=0.5.4.0- , base >=4.13.0.0 && <4.15+ , base >=4.13.0.0 && <5.0 , binary-parser >=0.5.6 , bytestring >=0.10.10.0 , bytestring-strict-builder >=0.4.5.3 , containers >=0.6.2.1 , contravariant >=1.5.2- , generic-random <1.3.1+ , generic-random , hedgehog >=1.0.3 , mtl >=2.2.2 , postgresql-binary >=0.12.2@@ -143,6 +144,7 @@ , th-lift-instances >=0.1.17 , time >=1.9.3 , transformers >=0.5.6.2+ , unordered-containers , uuid >=1.3.13 , vector >=0.12.1.2 , vector-sized >=1.4.1@@ -162,7 +164,7 @@ build-depends: aeson >=1.4.7.1 , array >=0.5.4.0- , base >=4.13.0.0 && <4.15+ , base >=4.13.0.0 && <5.0 , binary-parser >=0.5.6 , bytestring >=0.10.10.0 , bytestring-strict-builder >=0.4.5.3@@ -180,6 +182,7 @@ , th-lift-instances >=0.1.17 , time >=1.9.3 , transformers >=0.5.6.2+ , unordered-containers , uuid >=1.3.13 , vector >=0.12.1.2 , vector-sized >=1.4.1
src/Preql.hs view
@@ -1,5 +1,7 @@ {-# OPTIONS_GHC -fno-warn-duplicate-exports #-}--- | Description: Import this to start++-- | Description: Import this+ module Preql ( SQL(..), SqlQuery(..), sql, select, validSql , Transaction, Query
src/Preql/Effect.hs view
@@ -24,7 +24,6 @@ import Control.Monad.Trans.Except (ExceptT(..), runExceptT) import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT) import Control.Monad.Trans.Reader (ReaderT(..), ask, runReaderT)-import Database.PostgreSQL.LibPQ (Connection) import GHC.TypeNats import qualified Control.Monad.Trans.RWS.Lazy as L import qualified Control.Monad.Trans.RWS.Strict as S
src/Preql/Effect/Internal.hs view
@@ -10,7 +10,6 @@ import Control.Monad.Trans.Except (ExceptT(..)) import Control.Monad.Trans.Reader (ReaderT(..))-import Database.PostgreSQL.LibPQ (Connection) -- | A Transaction can only contain SQL queries (and pure functions). newtype Transaction a = Transaction (ExceptT QueryError (ReaderT Connection IO) a)
src/Preql/FromSql.hs view
@@ -4,4 +4,4 @@ import Preql.FromSql.Class as X import Preql.FromSql.Instances as X-import Preql.FromSql.TH as X+import Preql.FromSql.Tuple as X
src/Preql/FromSql/Class.hs view
@@ -21,8 +21,14 @@ -- Postgres type which can be decoded, and a parser from the binary -- representation of that type to the Haskell representation. data FieldDecoder a = FieldDecoder PgType (BP.BinaryParser a)- deriving Functor+ deriving Functor +fieldParser :: FieldDecoder a -> BP.BinaryParser a+fieldParser (FieldDecoder _ parser) = parser++-- | A type which can be decoded from a single SQL field. This is+-- mostly useful for defining what can be an element of an array or+-- 'Tuple'. class FromSqlField a where fromSqlField :: FieldDecoder a @@ -30,7 +36,8 @@ -- includes the canonical order of fields. -- -- The default (empty) instance works for any type with a--- 'FromSqlField' instance+-- 'FromSqlField' instance. This is convenient when you define your+-- own Postgres types, since they should be instances of both type classes. class FromSql a where -- | The number of columns read in decoding this type. type Width a :: Nat
src/Preql/FromSql/Instances.hs view
@@ -5,67 +5,78 @@ {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} -module Preql.FromSql.Instances where+module Preql.FromSql.Instances (fromSqlJsonField) where -import Preql.FromSql.Class-import Preql.FromSql.TH-import Preql.Wire.Errors-import Preql.Wire.Internal (applyDecoder)-import Preql.Wire.Types+import Preql.FromSql.Class+import Preql.FromSql.TH+import Preql.FromSql.Tuple+import Preql.Wire.Errors+import Preql.Wire.Internal (applyDecoder)+import Preql.Wire.Types -import qualified BinaryParser as BP-import qualified Data.Aeson as JSON-import qualified Data.ByteString as BS-import qualified Data.ByteString.Lazy as BSL-import Data.Int-import qualified Data.Text as T-import qualified Data.Text.Lazy as TL-import Data.Time (Day, TimeOfDay, UTCTime)-import Data.UUID (UUID)-import qualified Database.PostgreSQL.LibPQ as PQ-import GHC.TypeNats+import Data.Int+import Data.Time (Day, TimeOfDay, UTCTime)+import Data.UUID (UUID)+import Data.Vector (Vector)+import Database.PostgreSQL.LibPQ (Oid)+import GHC.TypeNats+import Preql.Imports+import qualified BinaryParser as BP+import qualified Data.Aeson as JSON+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BSL+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Data.Vector as V+import qualified Database.PostgreSQL.LibPQ as PQ import qualified PostgreSQL.Binary.Decoding as PGB-import Preql.Imports import qualified Preql.Wire.TypeInfo.Static as OID +-- We write @FromSql@ instances for each primitive type, rather than a single+-- @FromSqlField a => FromSql a@ in order to give better type errors. The+-- general instance above would overlap with every instance for a user-defined+-- type (for a table), and the error would suggest defining @FromSqlField@,+-- which is usually not the right answer. New field types (like enums) are much+-- less common than new tables, so this seems like a good tradeoff.+ instance FromSqlField Bool where- fromSqlField = FieldDecoder (Oid OID.boolOid) PGB.bool+ fromSqlField = FieldDecoder (Oid OID.boolOid OID.array_boolOid) PGB.bool instance FromSql Bool instance FromSqlField Int16 where- fromSqlField = FieldDecoder (Oid OID.int2Oid) PGB.int+ fromSqlField = FieldDecoder (Oid OID.int2Oid OID.array_int2Oid) PGB.int instance FromSql Int16 instance FromSqlField Int32 where- fromSqlField = FieldDecoder (Oid OID.int4Oid) PGB.int+ fromSqlField = FieldDecoder (Oid OID.int4Oid OID.array_int4Oid) PGB.int instance FromSql Int32 instance FromSqlField Int64 where- fromSqlField = FieldDecoder (Oid OID.int8Oid) PGB.int+ fromSqlField = FieldDecoder (Oid OID.int8Oid OID.array_int8Oid) PGB.int instance FromSql Int64 instance FromSqlField Float where- fromSqlField = FieldDecoder (Oid OID.float4Oid) PGB.float4+ fromSqlField = FieldDecoder (Oid OID.float4Oid OID.array_float4Oid) PGB.float4 instance FromSql Float instance FromSqlField Double where- fromSqlField = FieldDecoder (Oid OID.float8Oid) PGB.float8+ fromSqlField = FieldDecoder (Oid OID.float8Oid OID.array_float8Oid) PGB.float8 instance FromSql Double instance FromSqlField Char where- fromSqlField = FieldDecoder (Oid OID.charOid) PGB.char+ fromSqlField = FieldDecoder (Oid OID.charOid OID.array_charOid) PGB.char instance FromSql Char where fromSql = notNull fromSqlField -instance FromSqlField String where- fromSqlField = FieldDecoder (Oid OID.textOid) (T.unpack <$> PGB.text_strict)-instance FromSql String+instance {-# OVERLAPS #-} FromSqlField String where+ fromSqlField = FieldDecoder (Oid OID.textOid OID.array_textOid) (T.unpack <$> PGB.text_strict)+instance {-# OVERLAPS #-} FromSql String instance FromSqlField Text where- fromSqlField = FieldDecoder (Oid OID.textOid) PGB.text_strict+ fromSqlField = FieldDecoder (Oid OID.textOid OID.array_textOid) PGB.text_strict instance FromSql Text instance FromSqlField TL.Text where- fromSqlField = FieldDecoder (Oid OID.textOid) PGB.text_lazy+ fromSqlField = FieldDecoder (Oid OID.textOid OID.array_textOid) PGB.text_lazy instance FromSql TL.Text -- | If you want to encode some more specific Haskell type via JSON,@@ -73,51 +84,51 @@ -- 'PostgreSQL.Binary.Encoding.jsonb_bytes' directly, rather than this -- instance. instance FromSqlField ByteString where- fromSqlField = FieldDecoder (Oid OID.byteaOid) (BS.copy <$> BP.remainders)+ fromSqlField = FieldDecoder (Oid OID.byteaOid OID.array_byteaOid) (BS.copy <$> BP.remainders) instance FromSql ByteString instance FromSqlField BSL.ByteString where- fromSqlField = FieldDecoder (Oid OID.byteaOid) (BSL.fromStrict . BS.copy <$> BP.remainders)+ fromSqlField = FieldDecoder (Oid OID.byteaOid OID.array_byteaOid) (BSL.fromStrict . BS.copy <$> BP.remainders) instance FromSql BSL.ByteString -- TODO check for integer_datetimes setting instance FromSqlField UTCTime where- fromSqlField = FieldDecoder (Oid OID.timestamptzOid) PGB.timestamptz_int+ fromSqlField = FieldDecoder (Oid OID.timestamptzOid OID.array_timestamptzOid) PGB.timestamptz_int instance FromSql UTCTime instance FromSqlField Day where- fromSqlField = FieldDecoder (Oid OID.dateOid) PGB.date+ fromSqlField = FieldDecoder (Oid OID.dateOid OID.array_dateOid) PGB.date instance FromSql Day instance FromSqlField TimeOfDay where- fromSqlField = FieldDecoder (Oid OID.timeOid) PGB.time_int+ fromSqlField = FieldDecoder (Oid OID.timeOid OID.array_timeOid) PGB.time_int instance FromSql TimeOfDay instance FromSqlField TimeTZ where- fromSqlField = FieldDecoder (Oid OID.timetzOid) (uncurry TimeTZ <$> PGB.timetz_int)+ fromSqlField = FieldDecoder (Oid OID.timetzOid OID.array_timetzOid) (uncurry TimeTZ <$> PGB.timetz_int) instance FromSql TimeTZ instance FromSqlField UUID where- fromSqlField = FieldDecoder (Oid OID.uuidOid) PGB.uuid+ fromSqlField = FieldDecoder (Oid OID.uuidOid OID.array_uuidOid) PGB.uuid instance FromSql UUID instance FromSqlField PQ.Oid where- fromSqlField = PQ.Oid <$> FieldDecoder (Oid OID.oidOid) PGB.int+ fromSqlField = PQ.Oid <$> FieldDecoder (Oid OID.oidOid OID.array_oidOid) PGB.int instance FromSql PQ.Oid instance FromSqlField PgName where- fromSqlField = FieldDecoder (Oid OID.nameOid) (PgName <$> PGB.text_strict)+ fromSqlField = FieldDecoder (Oid OID.nameOid OID.array_nameOid) (PgName <$> PGB.text_strict) instance FromSql PgName where fromSql = notNull fromSqlField -- | If you want to encode some more specific Haskell type via JSON, -- it is more efficient to use 'fromSqlJsonField' rather than this -- instance. instance FromSqlField JSON.Value where- fromSqlField = FieldDecoder (Oid OID.jsonbOid) PGB.jsonb_ast+ fromSqlField = FieldDecoder (Oid OID.jsonbOid OID.array_jsonbOid) PGB.jsonb_ast instance FromSql JSON.Value fromSqlJsonField :: JSON.FromJSON a => FieldDecoder a-fromSqlJsonField = FieldDecoder (Oid OID.jsonbOid)+fromSqlJsonField = FieldDecoder (Oid OID.jsonbOid OID.array_jsonbOid) (PGB.jsonb_bytes (first T.pack . JSON.eitherDecode . BSL.fromStrict)) -- Overlappable so applications can write Maybe for multi-field domain types@@ -158,3 +169,86 @@ $(deriveFromSqlTuple 23) $(deriveFromSqlTuple 24) $(deriveFromSqlTuple 25)++-- The array instances all overlap, because arrays can nest arbitrarily.+-- Unfortunately, this means a runtime error if an application programmer try to+-- nest arrays deeper than the instances that we provide. So we provide more+-- instances than we expect users to want.++arrayDecoder :: FromSqlField a => (PGB.Array a -> PGB.Array b) -> FieldDecoder b+arrayDecoder dims =+ FieldDecoder arrayType (PGB.array (dims (PGB.valueArray parser)))+ where+ FieldDecoder oneType parser = fromSqlField+ arrayType = case oneType of+ Oid _ oid -> Oid oid (PQ.Oid 0)+ TypeName name -> TypeName ("_" <> name)++dimV :: PGB.Array a -> PGB.Array (Vector a)+dimV = PGB.dimensionArray V.replicateM++dimL :: PGB.Array a -> PGB.Array [a]+dimL = PGB.dimensionArray replicateM++instance FromSqlField (Vector a) => FromSql (Vector a)+instance {-# OVERLAPPABLE #-} FromSqlField a => FromSqlField (Vector a) where+ fromSqlField = arrayDecoder dimV++instance {-# OVERLAPPABLE #-} FromSqlField a => FromSqlField (Vector (Vector a)) where+ fromSqlField = arrayDecoder (dimV . dimV)++instance {-# OVERLAPPABLE #-} FromSqlField a => FromSqlField (Vector (Vector (Vector a))) where+ fromSqlField = arrayDecoder (dimV . dimV . dimV)++instance {-# OVERLAPPABLE #-} FromSqlField a => FromSqlField (Vector (Vector (Vector (Vector a)))) where+ fromSqlField = arrayDecoder (dimV . dimV . dimV . dimV)++instance {-# OVERLAPPABLE #-} FromSqlField a => FromSqlField (Vector (Vector (Vector (Vector (Vector a))))) where+ fromSqlField = arrayDecoder (dimV . dimV . dimV . dimV . dimV)++instance FromSqlField [a] => FromSql [a]+instance {-# OVERLAPPABLE #-} FromSqlField a => FromSqlField [a] where+ fromSqlField = arrayDecoder dimL++instance {-# OVERLAPPABLE #-} FromSqlField a => FromSqlField [[a]] where+ fromSqlField = arrayDecoder (dimL . dimL)++instance {-# OVERLAPPABLE #-} FromSqlField a => FromSqlField [[[a]]] where+ fromSqlField = arrayDecoder (dimL . dimL . dimL)++instance {-# OVERLAPPABLE #-} FromSqlField a => FromSqlField [[[[a]]]] where+ fromSqlField = arrayDecoder (dimL . dimL . dimL . dimL)++instance {-# OVERLAPPABLE #-} FromSqlField a => FromSqlField [[[[[a]]]]] where+ fromSqlField = arrayDecoder (dimL . dimL . dimL . dimL . dimL)++instance (FromSqlField a, FromSqlField b) => FromSqlField (Tuple (a, b)) where+ fromSqlField =+ let vc = valueComposite+ in FieldDecoder (Oid OID.recordOid OID.array_recordOid)+ (Tuple <$> composite 2 (pure (,) <*> vc fromSqlField <*> vc fromSqlField))+instance (FromSqlField a, FromSqlField b) => FromSql (Tuple (a, b))++$(deriveFromSqlFieldTuple 3)+$(deriveFromSqlFieldTuple 4)+$(deriveFromSqlFieldTuple 5)+$(deriveFromSqlFieldTuple 6)+$(deriveFromSqlFieldTuple 7)+$(deriveFromSqlFieldTuple 8)+$(deriveFromSqlFieldTuple 9)+$(deriveFromSqlFieldTuple 10)+$(deriveFromSqlFieldTuple 11)+$(deriveFromSqlFieldTuple 12)+$(deriveFromSqlFieldTuple 13)+$(deriveFromSqlFieldTuple 14)+$(deriveFromSqlFieldTuple 15)+$(deriveFromSqlFieldTuple 16)+$(deriveFromSqlFieldTuple 17)+$(deriveFromSqlFieldTuple 18)+$(deriveFromSqlFieldTuple 19)+$(deriveFromSqlFieldTuple 20)+$(deriveFromSqlFieldTuple 21)+$(deriveFromSqlFieldTuple 22)+$(deriveFromSqlFieldTuple 23)+$(deriveFromSqlFieldTuple 24)+$(deriveFromSqlFieldTuple 25)
src/Preql/FromSql/TH.hs view
@@ -6,12 +6,17 @@ module Preql.FromSql.TH where import Preql.FromSql.Class+import Preql.FromSql.Tuple import Preql.QuasiQuoter.Common (alphabet)+import Preql.Wire.Errors (PgType(Oid)) import Preql.Wire.Internal+import qualified Preql.Wire.TypeInfo.Static as OID import GHC.TypeNats import Language.Haskell.TH+import qualified PostgreSQL.Binary.Decoding as PGB +-- | instance (FromSql a, FromSql b) => FromSql (a, b) deriveFromSqlTuple :: Int -> Q [Dec] deriveFromSqlTuple n = do names <- traverse newName (take n alphabet)@@ -20,6 +25,8 @@ tuple = foldl AppT (TupleT n) fields return [fromSqlDecl tuple (tupleDataName n) fields] +-- | derive a 'FromSql' instance for a record type+-- (field names are not required, but there must be only one constructor) deriveFromSql :: Name -> Q [Dec] deriveFromSql tyName = do info <- reify tyName@@ -37,10 +44,17 @@ in return [fromSqlDecl targetTy conN fieldTypes] _ -> error ("deriveFromSql only handles type names, got: " ++ show tyName) +#if MIN_VERSION_template_haskell(2,17,0)+tyVarName :: TyVarBndr () -> Name+tyVarName = \case+ PlainTV name () -> name+ KindedTV name () _k -> name+#else tyVarName :: TyVarBndr -> Name tyVarName = \case PlainTV name -> name KindedTV name _k -> name+#endif fromSqlDecl :: Type -> Name -> [Type] -> Dec@@ -59,6 +73,30 @@ (VarE 'pureDecoder `AppE` ConE constructor) (replicate (length fields) (VarE 'fromSql)))) [] -- no where clause on the fromSql definition++-- instance (FromSqlField a, FromSqlField b) => FromSqlField (Tuple (a, b))+-- instance (FromSqlField a, FromSqlField b) => FromSql (Tuple (a, b))+deriveFromSqlFieldTuple :: Int -> Q [Dec]+deriveFromSqlFieldTuple n = do+ names <- traverse newName (take n alphabet)+ fieldOid <- [e| Oid OID.recordOid OID.array_recordOid |]+ let+ fields = map VarT names+ tuple = ConT ''Tuple `AppT` foldl AppT (TupleT n) fields+ context = [ ConT ''FromSqlField `AppT` ty | ty <- fields ]+ width = TySynEqn Nothing (ConT ''Width `AppT` tuple) (LitT (NumTyLit 1))+ tupleSizeE = LitE (IntegerL (toInteger n))+ parser = VarE 'fmap `AppE` ConE 'Tuple `AppE` (VarE 'composite `AppE` tupleSizeE `AppE` foldl+ (\parser field -> VarE '(<*>) `AppE` parser `AppE` field)+ (VarE 'pure `AppE` ConE (tupleDataName n))+ (replicate n (VarE 'valueComposite `AppE` VarE 'fromSqlField)))+ method = ValD+ (VarP 'fromSqlField)+ (NormalB (ConE 'FieldDecoder `AppE` fieldOid `AppE` parser))+ []+ return [ InstanceD Nothing context (ConT ''FromSqlField `AppT` tuple) [method]+ , InstanceD Nothing context (ConT ''FromSql `AppT` tuple) [ ] ]+ hasTyVar :: Type -> Bool hasTyVar = \case
+ src/Preql/FromSql/Tuple.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE ScopedTypeVariables #-}+-- | Description: newtype to support FromSql instances for row types.++module Preql.FromSql.Tuple where++import Preql.FromSql.Class (FieldDecoder(..))+import Preql.Wire.Errors (PgType(..))++import Control.Monad (unless)+import Data.Bits ((.|.), Bits, shiftL)+import Data.Int (Int32)+import qualified BinaryParser as BP+import qualified Data.ByteString as BS+import qualified Data.Text as T+import qualified Database.PostgreSQL.LibPQ as PQ++-- | Wrapper for Postgres anonymous row types (sometimes called record+-- types), so instance resolution picks the right decoder. The useful+-- instances are for (Haskell) tuples. Postgres allows row types with+-- a single field, but the instances would overlap with those for+-- nested row types, so we do not provide them.+newtype Tuple r = Tuple r+ deriving (Eq, Ord, Show)++-- Unlike the same-named functions in PostgreSQL.Binary.Decoding, these check+-- the number of components and the OIDs of each component.++-- | Helper for decoding composites+newtype Composite a = Composite (BP.BinaryParser a)+ deriving newtype (Functor, Applicative)++composite :: Int -> Composite a -> BP.BinaryParser a+composite n (Composite parser) = do+ size <- intOfSize 4+ unless (size == n) (BP.failure "composite has wrong size")+ parser++valueComposite :: FieldDecoder a -> Composite a+valueComposite (FieldDecoder pgType parser) = Composite $ do+ case pgType of+ -- For now, we only confirm statically-known OIDs. To do more, we'll need+ -- to feed through the OID cache (not yet implemented) and the Connection+ -- (which will be a problem if we ever implement lazy decoding)+ -- Statically-known OIDs seems like a reasonable compromise for now, until+ -- other parts of the design stabilize.+ Oid (PQ.Oid expected) _ -> do+ actual <- intOfSize 4+ unless (actual == expected) (BP.failure $ "OID in composite expected=" <> showt expected <> " actual=" <> showt actual)+ TypeName _ -> BP.unitOfSize 4 -- TODO check cache, maybe query DB+ onContent parser >>=+ maybe (BP.failure "unexpected null") pure+ where showt = T.pack . show++{-# INLINE intOfSize #-}+intOfSize :: (Integral a, Bits a) => Int -> BP.BinaryParser a+intOfSize x =+ fmap (BS.foldl' (\n h -> shiftL n 8 .|. fromIntegral h) 0) (BP.bytesOfSize x)++{-# INLINABLE onContent #-}+onContent :: BP.BinaryParser a -> BP.BinaryParser ( Maybe a )+onContent decoder = do+ size :: Int32 <- intOfSize 4+ case size of+ (-1) -> pure Nothing+ n -> fmap Just (BP.sized (fromIntegral n) decoder)
src/Preql/Wire.hs view
@@ -11,12 +11,12 @@ , ToSql(..), ToSqlField -- * Errors , QueryError(..), FieldError(..), UnlocatedFieldError(..), TypeMismatch(..)+ , Tuple(..) , module X) where -import Preql.Wire.Decode as X import Preql.Wire.Errors as X import Preql.Wire.Internal as X (Query, RowDecoder) import Preql.Wire.ToSql as X import Preql.Wire.Types as X-import Preql.Wire.Query as X (IsolationLevel(..))-import Preql.FromSql.Class as X+import Preql.Wire.Query as X (Connection, IsolationLevel(..), decodeVector)+import Preql.FromSql as X
− src/Preql/Wire/Decode.hs
@@ -1,42 +0,0 @@-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE UndecidableInstances #-}---- | Decoding values from Postgres wire format to Haskell.--module Preql.Wire.Decode where--import Preql.Wire.Errors-import Preql.Wire.Internal--import Control.Exception (try)-import Control.Monad.Except-import Data.IORef (newIORef)-import GHC.TypeNats-import Preql.Imports--import qualified Data.Vector as V-import qualified Data.Vector.Sized as VS-import qualified Database.PostgreSQL.LibPQ as PQ--decodeVector :: KnownNat n =>- (PgType -> IO (Either QueryError PQ.Oid)) -> RowDecoder n a -> PQ.Result -> IO (Either QueryError (Vector a))-decodeVector lookupType rd@(RowDecoder pgtypes _parsers) result = do- mismatches <- fmap (catMaybes . VS.toList) $ for (VS.zip (VS.enumFromN 0) pgtypes) $ \(column@(PQ.Col cint), expected) -> do- actual <- PQ.ftype result column- e_expectedOid <- lookupType expected- case e_expectedOid of- Right oid | actual == oid -> return Nothing- _ -> do- m_name <- liftIO $ PQ.fname result column- let columnName = decodeUtf8With lenientDecode <$> m_name- return $ Just (TypeMismatch{column = fromIntegral cint, ..})- if not (null mismatches)- then return (Left (PgTypeMismatch mismatches))- else do- (PQ.Row ntuples) <- liftIO $ PQ.ntuples result- ref <- newIORef (DecoderState result 0 0)- fmap (first DecoderError) . try $- V.replicateM (fromIntegral ntuples) (decodeRow ref rd result)
src/Preql/Wire/Errors.hs view
@@ -27,7 +27,7 @@ instance Exception FieldError $(deriveJSON defaultOptions ''FieldError) -data PgType = Oid PQ.Oid -- ^ A Postgres type with a known ID+data PgType = Oid PQ.Oid PQ.Oid -- ^ A Postgres type with a known ID, and the matching array ID | TypeName Text -- ^ A Postgres type which we will need to lookup by name deriving (Eq, Show, Typeable) $(deriveJSON defaultOptions ''PgType)
src/Preql/Wire/Query.hs view
@@ -1,45 +1,78 @@+-- | Description: Send queries, decode results, look up OID for a known type name++{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} module Preql.Wire.Query where import Preql.FromSql-import Preql.Wire.Decode import Preql.Wire.Errors import Preql.Wire.Internal import Preql.Wire.ToSql +import Control.Exception (try) import Control.Monad+import Control.Monad.Except+import Data.IORef import GHC.TypeNats import Preql.Imports+import System.IO.Unsafe (unsafePerformIO) import Debug.Trace +import qualified Data.HashMap.Strict as HM import qualified Data.Text as T import qualified Data.Vector as V+import qualified Data.Vector.Sized as VS import qualified Database.PostgreSQL.LibPQ as PQ +-- Cache of OIDs by type name++type TypeCache = IORef (HM.HashMap Text PQ.Oid)++-- | We make the type cache part of the Connection to offer the option of+-- per-Connection (or striped) caches. It's also reasonable to share a single+-- cache for an entire multi-threaded program; the @IORef@ supports this usage.+data Connection = Connection+ { rawConnection :: !PQ.Connection+ , typeCache :: !TypeCache+ }++connectdbSharedCache :: ByteString -> IO Connection+connectdbSharedCache str = Connection <$> PQ.connectdb str <*> pure globalCache++connectdbNewCache :: ByteString -> IO Connection+connectdbNewCache str = Connection <$> PQ.connectdb str <*> newIORef mempty++finish :: Connection -> IO ()+finish (Connection conn _) = PQ.finish conn++globalCache :: TypeCache+globalCache = unsafePerformIO (newIORef mempty)++-- send queries, receiving results+ queryWith :: KnownNat (Width r) =>- RowEncoder p -> RowDecoder (Width r) r -> PQ.Connection ->+ RowEncoder p -> RowDecoder (Width r) r -> Connection -> Query (Width r) -> p -> IO (Either QueryError (Vector r))-queryWith enc dec conn (Query q) params = do- -- TODO safer Connection type- -- withMVar (connectionHandle conn) $ \connRaw -> do- e_result <- execParams enc conn q params- traceEventIO "execParams > decodeVector"- case e_result of- Left err -> return (Left err)- Right rows -> decodeVector (lookupType conn) dec rows+queryWith enc dec conn@(Connection pqConn cache) (Query q) params = do+ e_result <- execParams enc pqConn q params+ traceEventIO "execParams > decodeVector"+ case e_result of+ Left err -> return (Left err)+ Right rows -> decodeVector conn dec rows -- If there is no result, we don't need a Decoder-queryWith_ :: RowEncoder p -> PQ.Connection -> Query n -> p -> IO (Either QueryError ())-queryWith_ enc conn (Query q) params = do+queryWith_ :: RowEncoder p -> Connection -> Query n -> p -> IO (Either QueryError ())+queryWith_ enc (Connection conn _) (Query q) params = do e_result <- execParams enc conn q params return (void e_result) query :: (ToSql p, FromSql r, KnownNat (Width r)) =>- PQ.Connection -> Query (Width r) -> p -> IO (Either QueryError (Vector r))+ Connection -> Query (Width r) -> p -> IO (Either QueryError (Vector r)) query = queryWith toSql fromSql -query_ :: ToSql p => PQ.Connection -> Query n -> p -> IO (Either QueryError ())+query_ :: ToSql p => Connection -> Query n -> p -> IO (Either QueryError ()) query_ = queryWith_ toSql execParams :: RowEncoder p -> PQ.Connection -> ByteString -> p -> IO (Either QueryError PQ.Result)@@ -64,29 +97,77 @@ Just msg -> return (Left (decodeUtf8With lenientDecode msg)) Nothing -> return (Left "No error message available") -lookupType :: PQ.Connection -> PgType -> IO (Either QueryError PQ.Oid)-lookupType _ (Oid oid) = return (Right oid)-lookupType conn (TypeName name) = do- e_rows <- query conn "SELECT oid FROM pg_type WHERE typname = $1" name- case fmap (V.!? 0) e_rows of- Left e -> return (Left e)- Right (Just oid) -> return (Right oid)- Right Nothing -> return (Left (ConnectionError ("No oid for: " <> name)))+-- decoding +decodeVector :: KnownNat n =>+ Connection -> RowDecoder n a -> PQ.Result -> IO (Either QueryError (Vector a))+decodeVector conn rd@(RowDecoder pgtypes _parsers) result = do+ mismatches <- fmap (catMaybes . VS.toList) $ for (VS.zip (VS.enumFromN 0) pgtypes) $ \(column@(PQ.Col cint), expected) -> do+ actual <- PQ.ftype result column+ lookupResult <- lookupType conn expected+ let mismatch = do+ m_name <- liftIO $ PQ.fname result column+ let columnName = decodeUtf8With lenientDecode <$> m_name+ return $ Just (TypeMismatch{column = fromIntegral cint, ..})+ case lookupResult of+ Cached oid | actual == oid -> return Nothing+ FromDb oid | actual == oid -> return Nothing+ Cached _ -> do -- recheck DB, in case type changed under us+ e_oid <- lookupTypeIgnoreCache conn expected+ case e_oid of+ Right oid | actual == oid -> return Nothing+ _ -> mismatch+ _ -> mismatch+ if not (null mismatches)+ then return (Left (PgTypeMismatch mismatches))+ else do+ (PQ.Row ntuples) <- liftIO $ PQ.ntuples result+ ref <- newIORef (DecoderState result 0 0)+ fmap (first DecoderError) . try $+ V.replicateM (fromIntegral ntuples) (decodeRow ref rd result)+ where++lookupType :: Connection -> PgType -> IO LookupResult+lookupType _ (Oid oid _) = return (FromDb oid)+lookupType conn@(Connection _ cacheRef) expected@(TypeName name) = do+ cache <- readIORef cacheRef+ case HM.lookup name cache of+ Just oid -> return (Cached oid)+ Nothing -> lookupTypeIgnoreCache conn expected+ <&> either LookupError FromDb++data LookupResult+ = Cached PQ.Oid+ | FromDb PQ.Oid+ | LookupError QueryError++lookupTypeIgnoreCache :: Connection -> PgType -> IO (Either QueryError PQ.Oid)+lookupTypeIgnoreCache _ (Oid oid _) = return (Right oid)+lookupTypeIgnoreCache conn@(Connection _ cacheRef) (TypeName name) = do+ e_rows <- query conn "SELECT oid FROM pg_type WHERE typname = $1" name+ case fmap (V.!? 0) e_rows of+ Left e -> return (Left e)+ Right (Just oid) -> do+ atomicModifyIORef cacheRef (\cache -> (HM.insert name oid cache, ()))+ return (Right oid)+ Right Nothing -> return (Left (ConnectionError ("No oid for: " <> name)))++-- transactions+ data IsolationLevel = ReadCommitted | RepeatableRead | Serializable deriving (Show, Read, Eq, Ord, Enum, Bounded) -begin :: PQ.Connection -> IsolationLevel -> IO (Either QueryError ())+begin :: Connection -> IsolationLevel -> IO (Either QueryError ()) begin conn level = query_ conn q () where q = case level of ReadCommitted -> "BEGIN ISOLATION LEVEL READ COMMITTED" RepeatableRead -> "BEGIN ISOLATION LEVEL REPEATABLE READ" Serializable -> "BEGIN ISOLATION LEVEL SERIALIZABLE" -commit :: PQ.Connection -> IO (Either QueryError ())+commit :: Connection -> IO (Either QueryError ()) commit conn = query_ conn "COMMIT" () -rollback :: PQ.Connection -> IO (Either QueryError ())+rollback :: Connection -> IO (Either QueryError ()) rollback conn = query_ conn "ROLLBACK" ()
test/Test.hs view
@@ -43,7 +43,7 @@ ] integration :: TestTree-integration = withResource initDB PQ.finish $ \db ->+integration = withResource initDB W.finish $ \db -> let query' :: (ToSql p, FromSql r, KnownNat (Width r)) => (Preql.Query (Width r), p) -> IO (Vector r)@@ -67,10 +67,10 @@ assertEqual "" [(1, "one")] (result :: Vector (Int32, T.Text)) ] -initDB :: HasCallStack => IO PQ.Connection+initDB :: HasCallStack => IO Connection initDB = do- conn <- PQ.connectdb =<< connectionString- status <- PQ.status conn+ conn@(W.Connection rawConn _) <- W.connectdbSharedCache =<< connectionString+ status <- PQ.status rawConn unless (status == PQ.ConnectionOk) (throwIO =<< badConnection conn) let query' q = either throwIO return =<< W.query_ conn q () query' "DROP TABLE IF EXISTS baz"
test/Test/Wire.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE CPP #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-}@@ -12,14 +13,16 @@ import Control.Exception (Exception, bracket_, throwIO) import Control.Monad+import Data.Bits (shiftR) import Data.ByteString (ByteString) import Data.Either import Data.Int import Data.Maybe (fromMaybe) import Data.Text (Text)-import Data.Text.Encoding (encodeUtf8)+import Data.Text.Encoding (decodeUtf8With, encodeUtf8) import Data.Time (Day, TimeOfDay, UTCTime) import Data.Vector (Vector)+import Data.Word (Word8) import GHC.TypeNats import System.Environment (lookupEnv) import Test.Tasty@@ -27,15 +30,19 @@ import Data.Time.Format.ISO8601 (iso8601ParseM) +import qualified BinaryParser as BP+import qualified Data.ByteString as BS import qualified Data.Text as T+import qualified Data.Text.Encoding.Error as T import qualified Data.Text.Lazy as TL import qualified Data.UUID as UUID import qualified Database.PostgreSQL.LibPQ as PQ import qualified PostgreSQL.Binary.Decoding as PGB import qualified Preql.Wire.Query as W+import qualified Preql.Wire.TypeInfo.Static as OID wire :: TestTree-wire = withResource initDB PQ.finish $ \db -> testGroup "wire" $+wire = withResource initDB W.finish $ \db -> testGroup "wire" $ let inTransaction desc body = testCase desc $ bracket_ (query_ "BEGIN TRANSACTION" ()) (query_ "ROLLBACK" ()) body@@ -194,13 +201,54 @@ query_ "create type foo as (bar bool, baz int)" () result <- query "select row(true, 1)::foo as foo" () assertEqual "" (Right [Foo True 1]) result+ , inTransaction "anonymous row type (bool, int), row() syntax" $ do+ result <- query "select row(true, 1)" ()+ assertEqual "" (Right [Tuple (True, 1::Int32)]) result+ , inTransaction "anonymous row type (bool, int), tuple syntax" $ do+ result <- query "select (true, 1)" ()+ assertEqual "" (Right [Tuple (True, 1::Int32)]) result+ , inTransaction "longer derived row type" $ do+ result <- query "select (true, 1, 'foo'::text)" ()+ assertEqual "" (Right [Tuple (True, 1::Int32, "foo"::Text)]) result+ , inTransaction "nested row types" $ do+ result <- query "select (true, row(1, 2))" ()+ assertEqual "" (Right [Tuple (True, Tuple (1::Int32, 2::Int32))]) result ]+ , testGroup "array types"+ [ inTransaction "text array" $ do+ result <- query "select '{foo, bar, baz}'::text[]" ()+ assertEqual "" (Right [["foo", "bar", "baz"] :: [Text]]) result+ , inTransaction "nested array" $ do+ result <- query "select '{ {1, 2, 3}, {4, 5, 6} }'::int[][]" ()+ assertEqual "" (Right [ [[1, 2, 3], [4, 5, 6]]:: [[Int32]] ] ) result+ , inTransaction "vector array" $ do+ result <- query "select '{foo, bar, baz}'::text[]" ()+ assertEqual "" (Right [["foo", "bar", "baz"] :: Vector Text]) result+ , inTransaction "array in row type" $ do+ result <- query "select row(true, '{1, 2}'::int[])" ()+ -- assertEqual "" (Right [ Debug "" ]) result+ assertEqual "" (Right [ Tuple (True, [1,2] :: [Int32]) ]) result+ , inTransaction "array of row type" $ do+ result <- query "select ARRAY[row(true, 'foo'::text), row(false, 'bar'::text)]" ()+ assertEqual "" (Right [ [Tuple (True, "foo"), Tuple (False, "bar")] :: [Tuple (Bool, Text)] ]) result+ ] ] -initDB :: HasCallStack => IO PQ.Connection+newtype Debug = Debug ByteString+ deriving Eq++instance Show Debug where+ show (Debug bs) = "text '" ++ T.unpack (decodeUtf8With T.lenientDecode bs)+ ++ "' hex 0x" ++ concatMap hex (BS.unpack bs)++instance FromSqlField Debug where+ fromSqlField = FieldDecoder (Oid OID.recordOid OID.array_recordOid) (Debug <$> BP.remainders)+instance FromSql Debug++initDB :: HasCallStack => IO Connection initDB = do- conn <- PQ.connectdb =<< connectionString- status <- PQ.status conn+ conn@(W.Connection rawConn _) <- W.connectdbSharedCache =<< connectionString+ status <- PQ.status rawConn unless (status == PQ.ConnectionOk) (throwIO =<< badConnection conn) void $ W.query_ conn "DROP TABLE IF EXISTS encoder_tests" () void $ W.query_ conn "CREATE TABLE encoder_tests (b boolean, i16 int2, i32 int4, i64 int8, f float4, d float8, t text, by bytea, ts timestamptz, day date, time time, ttz timetz, u uuid)" ()@@ -224,8 +272,8 @@ deriving (Show) instance Exception BadConnection -badConnection :: PQ.Connection -> IO BadConnection-badConnection c = do+badConnection :: Connection -> IO BadConnection+badConnection (W.Connection c _) = do status <- PQ.status c errorMessage <- fromMaybe "" <$> PQ.errorMessage c host <- fromMaybe "" <$> PQ.host c@@ -245,3 +293,25 @@ expt :: Num a => a -> Int64 -> a expt = (^)++hex :: Word8 -> String+hex byte = [ nibble hi, nibble lo ] where+ lo = byte `mod` 16+ hi = byte `shiftR` 4+ nibble = \case+ 0 -> '0'+ 1 -> '1'+ 2 -> '2'+ 3 -> '3'+ 4 -> '4'+ 5 -> '5'+ 6 -> '6'+ 7 -> '7'+ 8 -> '8'+ 9 -> '9'+ 10 -> 'a'+ 11 -> 'b'+ 12 -> 'c'+ 13 -> 'd'+ 14 -> 'e'+ 15 -> 'f'