packages feed

composite-opaleye (empty) → 0.1.0.0

raw patch · 7 files changed

+359/−0 lines, 7 filesdep +Framesdep +basedep +basic-preludesetup-changed

Dependencies added: Frames, base, basic-prelude, bytestring, composite-base, lens, opaleye, postgresql-simple, product-profunctors, profunctors, template-haskell, text, vinyl

Files

+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ composite-opaleye.cabal view
@@ -0,0 +1,43 @@+-- This file has been generated from package.yaml by hpack version 0.15.0.+--+-- see: https://github.com/sol/hpack++name:           composite-opaleye+version:        0.1.0.0+synopsis:       Opaleye SQL for Frames records+description:    Integration between Frames records and Opaleye SQL, allowing records to be stored, retrieved, and queried from PostgreSQL.+category:       Records+homepage:       https://github.com/ConferHealth/composite#readme+author:         Confer Health, Inc.+maintainer:     oss@confer.care+copyright:      2017 Confer Health, Inc.+license:        BSD3+build-type:     Simple+cabal-version:  >= 1.10++library+  hs-source-dirs:+      src+  default-extensions: DataKinds FlexibleContexts FlexibleInstances LambdaCase MultiParamTypeClasses NoImplicitPrelude OverloadedStrings PolyKinds ScopedTypeVariables StrictData TemplateHaskell TypeFamilies TypeOperators ViewPatterns+  ghc-options: -Wall -O2+  build-depends:+      base >= 4.7 && < 5+    , Frames+    , basic-prelude+    , bytestring+    , composite-base+    , lens+    , opaleye+    , postgresql-simple+    , product-profunctors+    , profunctors+    , template-haskell+    , text+    , vinyl+  exposed-modules:+      Composite.Opaleye+      Composite.Opaleye.ProductProfunctors+      Composite.Opaleye.RecordTable+      Composite.Opaleye.TH+      Composite.Opaleye.Util+  default-language: Haskell2010
+ src/Composite/Opaleye.hs view
@@ -0,0 +1,7 @@+module Composite.Opaleye+  ( module Composite.Opaleye.ProductProfunctors+  , module Composite.Opaleye.RecordTable+  ) where++import Composite.Opaleye.ProductProfunctors+import Composite.Opaleye.RecordTable
+ src/Composite/Opaleye/ProductProfunctors.hs view
@@ -0,0 +1,60 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+module Composite.Opaleye.ProductProfunctors where++import BasicPrelude+import Data.Profunctor (dimap)+import Data.Profunctor.Product (ProductProfunctor, (***!))+import qualified Data.Profunctor.Product as PP+import Data.Profunctor.Product.Default (Default(def))+import Data.Vinyl.Core (Rec((:&), RNil))+import Data.Vinyl.Functor (Identity(Identity))+-- FIXME would nice to be generic to all vinyl records but then the types get really really hairy trying to assert Unwrapped r ~ p a b, project out a, and so on+import Frames ((:->)(Col))++-- |Type class implementing traversal of a record, yanking individual product profunctors @Record [p a b]@+-- (though with distinct @a@ and @b@ at each position) up to @p (Record as) (Record bs)@.+--+-- This is similar to the @pN@ functions on tuples provided by the @product-profunctors@ library.+class ProductProfunctor p => PRec p rs where+  -- |Record fields @rs@ with the profunctor removed yielding the contravariant parameter. E.g. @PRecContra p '[p a b] ~ '[a]@+  type PRecContra p rs :: [*]+  -- |Record fields @rs@ with the profunctor removed yielding the covariant parameter. E.g. @PRecContra p '[p a b] ~ '[a]@+  type PRecCo     p rs :: [*]++  -- |Traverse the record, transposing the profunctors @p@ within to the outside like 'traverse' does for Applicative effects.+  --+  -- Roughly equivalent to @Record '[p a b, p c d, …] -> p (Record '[a, c, …]) (Record '[b, d, …])@+  pRec :: Rec Identity rs -> p (Rec Identity (PRecContra p rs)) (Rec Identity (PRecCo p rs))++instance ProductProfunctor p => PRec p '[] where+  type PRecContra p '[] = '[]+  type PRecCo     p '[] = '[]++  pRec RNil = dimap (const ()) (const RNil) PP.empty++instance (ProductProfunctor p, PRec p rs) => PRec p (s :-> p a b ': rs) where+  type PRecContra p (s :-> p a b ': rs) = (s :-> a ': PRecContra p rs)+  type PRecCo     p (s :-> p a b ': rs) = (s :-> b ': PRecCo     p rs)++  pRec (Identity (Col p) :& rs) =+    dimap (\ (Identity (Col a) :& aRs) -> (a, aRs))+          (\ (b, bRs) -> (Identity (Col b) :& bRs))+          (p ***! pRec rs)++instance ProductProfunctor p => Default p (Rec Identity '[]) (Rec Identity '[]) where+  def = dimap (const ()) (const RNil) PP.empty++instance forall p s a b rsContra rsCo. (ProductProfunctor p, Default p a b, Default p (Rec Identity rsContra) (Rec Identity rsCo))+      => Default p (Rec Identity (s :-> a ': rsContra)) (Rec Identity (s :-> b ': rsCo)) where+  def =+    dimap (\ (Identity (Col a) :& aRs) -> (a, aRs))+          (\ (b, bRs) -> (Identity (Col b) :& bRs))+          (step ***! recur)+    where+      step :: p a b+      step = def+      recur :: p (Rec Identity rsContra) (Rec Identity rsCo)+      recur = def+++
+ src/Composite/Opaleye/RecordTable.hs view
@@ -0,0 +1,67 @@+module Composite.Opaleye.RecordTable where++import BasicPrelude+import Composite.Base (NamedField(fieldName))+import Control.Lens (Wrapped(type Unwrapped, _Wrapped'), from, view)+import Data.Profunctor (dimap)+import Data.Profunctor.Product ((***!))+import qualified Data.Profunctor.Product as PP+import Data.Proxy (Proxy(Proxy))+import Data.Text (unpack)+import Data.Vinyl.Core (Rec((:&), RNil))+import Data.Vinyl.Functor (Identity(Identity))+import Opaleye (Column, TableProperties, required, optional)++-- |Helper typeclass which picks which of 'required' or 'optional' to use for a pair of write column type and read column type.+--+-- @DefaultRecTableField (Maybe (Column a)) (Column a)@ uses 'optional'.+-- @DefaultRecTableField        (Column a)  (Column a)@ uses 'required'.+class DefaultRecTableField write read where+  defaultRecTableField :: String -> TableProperties write read++instance DefaultRecTableField (Maybe (Column a)) (Column a) where+  defaultRecTableField = optional++instance DefaultRecTableField (Column a) (Column a) where+  defaultRecTableField = required++-- |Type class for producing a default 'TableProperties' schema for some expected record types. 'required' and 'optional' are chosen automatically and the+-- column is named after the record fields, using 'NamedField' to reflect the field names.+--+-- For example, given:+--+-- >  type WriteRec = Record '["id" :-> Maybe (Column PGInt8), "name" :-> Column PGText]+-- >  type ReadRec  = Record '["id" :->        Column PGInt8 , "name" :-> Column PGText]+--+-- This:+--+-- >  defaultRecTable :: TableProperties WriteRec ReadRec+--+-- Is equivalent to:+--+-- > pReq (optional "id" &: required "name" &: Nil)+--+--+-- Alternately, use 'Composite.Opaleye.ProductProfunctors.pReq' and the usual Opaleye 'required' and 'optional'.+class DefaultRecTable write read where+  defaultRecTable :: TableProperties (Rec Identity write) (Rec Identity read)++instance DefaultRecTable '[] '[] where+  defaultRecTable = dimap (const ()) (const RNil) PP.empty++instance+    forall r reads w writes.+    ( NamedField w, NamedField r+    , DefaultRecTableField (Unwrapped w) (Unwrapped r)+    , DefaultRecTable writes reads+    ) => DefaultRecTable (w ': writes) (r ': reads) where+  defaultRecTable =+    dimap (\ (Identity (view _Wrapped' -> w) :& writeRs) -> (w, writeRs))+          (\ (r, readRs) -> (Identity (view (from _Wrapped') r) :& readRs))+          (step  ***! recur)+    where+      step :: TableProperties (Unwrapped w) (Unwrapped r)+      step = defaultRecTableField . unpack $ fieldName (Proxy :: Proxy r)+      recur :: TableProperties (Rec Identity writes) (Rec Identity reads)+      recur = defaultRecTable+
+ src/Composite/Opaleye/TH.hs view
@@ -0,0 +1,169 @@+module Composite.Opaleye.TH where++import BasicPrelude hiding (lift)+import Composite.Opaleye.Util (constantColumnUsing)+import Control.Lens ((<&>))+import qualified Data.ByteString.Char8 as BSC8+import Data.Profunctor.Product.Default (Default, def)+import Data.Traversable (for)+import Database.PostgreSQL.Simple (ResultError(ConversionFailed, Incompatible, UnexpectedNull))+import Database.PostgreSQL.Simple.FromField (FromField, fromField, typename, returnError)+import Language.Haskell.TH+  ( Q, Name, mkName, nameBase, newName, pprint, reify+  , Info(TyConI), Dec(DataD), Con(NormalC)+  , conT+  , dataD, instanceD+  , lamE, varE, caseE, conE+  , conP, varP, wildP, litP, stringL+  , caseE, match+  , funD, clause+  , normalB, normalGE, guardedB+  , cxt+  )+import Language.Haskell.TH.Syntax (lift)+import Opaleye+  ( Column, Constant, QueryRunnerColumnDefault, PGText, fieldQueryRunnerColumn, queryRunnerColumnDefault+  )++-- |Derive the various instances required to make a Haskell enumeration map to a PostgreSQL @enum@ type.+--+-- In @deriveOpaleyeEnum ''HaskellType "sqltype" hsConToSqlValue@, @''HaskellType@ is the sum type (data declaration) to make instances for, @"sqltype"@ is+-- the PostgreSQL type name, and @hsConToSqlValue@ is a function to map names of constructors to SQL values.+--+-- The function @hsConToSqlValue@ is of the type @String -> Maybe String@ in order to make using 'stripPrefix' convenient. The function is applied to each+-- constructor name and for @Just value@ that value is used, otherwise for @Nothing@ the constructor name is used.+--+-- For example, given the Haskell type:+--+-- @+--     data MyEnum = MyFoo | MyBar+-- @+--+-- And PostgreSQL type:+--+-- @+--     CREATE TYPE myenum AS ENUM('foo', 'bar');+-- @+--+-- The splice:+--+-- @+--     deriveOpaleyeEnum ''MyEnum "myenum" ('stripPrefix' "my" . 'map' 'toLower')+-- @+--+-- Will create @PGMyEnum@ and instances required to use @MyEnum@ / @Column MyEnum@ in Opaleye.+--+-- The Haskell generated by this splice for the example is something like:+--+-- @+--     data PGMyEnum+--+--     instance 'FromField' MyEnum where+--       'fromField' f mbs = do+--         tname <- 'typename' f+--         case mbs of+--           _ | tname /= "myenum" -> 'returnError' 'Incompatible' f ""+--           Just "foo" -> pure MyFoo+--           Just "bar" -> pure MyBar+--           Just other -> 'returnError' 'ConversionFailed' f ("Unexpected myenum value: " <> 'BSC8.unpack' other)+--           Nothing    -> 'returnError' 'UnexpectedNull' f ""+--+--     instance 'QueryRunnerColumnDefault' PGMyEnum MyEnum where+--       queryRunnerColumnDefault = 'fieldQueryRunnerColumn'+--+--     instance 'Default' 'Constant' MyEnum ('Column' PGMyEnum) where+--       def = 'constantColumnUsing' (def :: 'Constant' String ('Column' 'PGText')) $ \ case+--         MyFoo -> "foo"+--         MyBar -> "bar"+-- @+deriveOpaleyeEnum :: Name -> String -> (String -> Maybe String) -> Q [Dec]+deriveOpaleyeEnum hsName sqlName hsConToSqlValue = do+  let sqlTypeName = mkName $ "PG" ++ nameBase hsName+      sqlType = conT sqlTypeName+      hsType = conT hsName++  rawCons <- reify hsName >>= \ case+    TyConI (DataD _cxt _name _tvVarBndrs _maybeKind cons _derivingCxt) ->+      pure cons+    other ->+      fail $ "expected " <> show hsName <> " to name a data declaration, not:\n" <> pprint other++  nullaryCons <- for rawCons $ \ case+    NormalC conName [] ->+      pure conName+    other ->+      fail $ "expected every constructor of " <> show hsName <> " to be a regular nullary constructor, not:\n" <> pprint other++  let conPairs = nullaryCons <&> \ conName ->+        (conName, fromMaybe (nameBase conName) (hsConToSqlValue (nameBase conName)))++  sqlTypeDecl <- dataD (cxt []) sqlTypeName [] Nothing [] (cxt [])++  fromFieldInst <- instanceD (cxt []) [t| FromField $hsType |] . (:[]) $ do+    field <- newName "field"+    mbs   <- newName "mbs"+    tname <- newName "tname"+    other <- newName "other"++    let bodyCase = caseE (varE mbs) $+          [ match+              wildP+              (guardedB [ normalGE [| $(varE tname) /= $(lift sqlName) |]+                                   [| returnError Incompatible $(varE field) "" |] ])+              []+          ] +++          (+            conPairs <&> \ (conName, value) ->+              match+                [p| Just $(litP $ stringL value) |]+                (normalB [| pure $(conE conName) |])+                []+          ) +++          [ match +              [p| Just $(varP other) |]+              (normalB [| returnError ConversionFailed $(varE field) ("Unexpected " <> $(lift sqlName) <> " value: " <> BSC8.unpack $(varE other)) |])+              []+          , match+              [p| Nothing |]+              (normalB [| returnError UnexpectedNull $(varE field) "" |])+              []+          ]++    funD 'fromField+      [ clause+          [varP field, varP mbs]+          (normalB [|+            do+              $(varP tname) <- typename $(varE field)+              $bodyCase+            |])+          []+      ]++  queryRunnerColumnDefaultInst <- instanceD (cxt []) [t| QueryRunnerColumnDefault $sqlType $hsType |] . (:[]) $+    funD 'queryRunnerColumnDefault+      [ clause+          []+          (normalB [| fieldQueryRunnerColumn |])+          []+      ]++  defaultInst <- instanceD (cxt []) [t| Default Constant $hsType (Column $sqlType) |] . (:[]) $ do+    s <- newName "s"+    let body = lamE [varP s] $+          caseE (varE s) $+            conPairs <&> \ (conName, value) ->+              match+                (conP conName [])+                (normalB $ lift value)+                []++    funD 'def+      [ clause+          []+          (normalB [| constantColumnUsing (def :: Constant String (Column PGText)) $body |])+          []+      ]++  pure [sqlTypeDecl, fromFieldInst, queryRunnerColumnDefaultInst, defaultInst]+
+ src/Composite/Opaleye/Util.hs view
@@ -0,0 +1,11 @@+module Composite.Opaleye.Util where++import Data.Profunctor (dimap)+import Opaleye (Column, Constant, unsafeCoerceColumn)++-- |Coerce one type of 'Column' 'Constant' profunctor to another using by just asserting the changed type on the column side and using the given function+-- on the Haskell side. Useful when the PG value representation is the same but the Haskell type changes, e.g. for enums.+constantColumnUsing :: Constant haskell (Column pgType)+                    -> (haskell' -> haskell)+                    -> Constant haskell' (Column pgType')+constantColumnUsing oldConstant f = dimap f unsafeCoerceColumn oldConstant