@@ -0,0 +1,84 @@
1+{-# language DataKinds #-}
2+{-# language FlexibleContexts #-}
3+{-# language GADTs #-}
4+{-# language ScopedTypeVariables #-}
5+{-# language StandaloneKindSignatures #-}
6+{-# language TypeApplications #-}
7+{-# language TypeOperators #-}
8+{-# language UndecidableInstances #-}
9+
10+module Rel8.Type.Composite
11+ ( Composite( Composite )
12+ )
13+where
14+
15+-- base
16+import Control.Applicative ( liftA2 )
17+import Data.Kind ( Constraint, Type )
18+import Data.Proxy ( Proxy( Proxy ) )
19+import GHC.Generics
20+ ( Generic, Rep, from, to
21+ , (:*:)( (:*:) ), K1( K1 ), M1( M1 )
22+ )
23+import GHC.TypeLits ( KnownSymbol, Symbol, symbolVal )
24+import Prelude hiding ( null )
25+
26+-- hasql
27+import qualified Hasql.Decoders as Hasql
28+
29+-- opaleye
30+import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
31+
32+-- rel8
33+import Rel8.Schema.Null ( Nullity( Null, NotNull ), Sql, nullable )
34+import Rel8.Type ( DBType, typeInformation )
35+import Rel8.Type.Information ( TypeInformation(..) )
36+
37+
38+-- | A deriving-via helper type for column types that store a Haskell product
39+-- type in a single Postgres column using a Postgres composite type.
40+--
41+-- Note that this must map to a specific extant type in your database's schema
42+-- (created with @CREATE TYPE@). The @name@ parameter of @Composite@ should be
43+-- the name of this type.
44+type Composite :: Symbol -> Type -> Type
45+newtype Composite name a = Composite
46+ { unComposite :: a
47+ }
48+
49+
50+instance (KnownSymbol name, Generic a, GComposite (Rep a)) =>
51+ DBType (Composite name a)
52+ where
53+ typeInformation = TypeInformation
54+ { decode = Hasql.composite (Composite . to <$> gdecode @(Rep a))
55+ , encode = Opaleye.FunExpr "ROW" . gencode @(Rep a) . from . unComposite
56+ , typeName = symbolVal (Proxy @name)
57+ }
58+
59+
60+type GComposite :: (Type -> Type) -> Constraint
61+class GComposite rep where
62+ gdecode :: Hasql.Composite (rep x)
63+ gencode :: rep x -> [Opaleye.PrimExpr]
64+
65+
66+instance GComposite rep => GComposite (M1 i c rep) where
67+ gdecode = M1 <$> gdecode
68+ gencode (M1 a) = gencode a
69+
70+
71+instance (GComposite a, GComposite b) => GComposite (a :*: b) where
72+ gdecode = liftA2 (:*:) gdecode gdecode
73+ gencode (a :*: b) = gencode a <> gencode b
74+
75+
76+instance Sql DBType a => GComposite (K1 i a) where
77+ gdecode = fmap K1 $ Hasql.field $ case nullable @a of
78+ Null -> Hasql.nullable (decode typeInformation)
79+ NotNull -> Hasql.nonNullable (decode typeInformation)
80+ gencode (K1 a) = case nullable @a of
81+ Null -> [maybe null (encode typeInformation) a]
82+ NotNull -> [encode typeInformation a]
83+ where
84+ null = Opaleye.ConstExpr Opaleye.NullLit