Skip to content

Commit afcc65c

Browse files
Add support for PostgreSQL's anonymous row types
1 parent fade0ca commit afcc65c

4 files changed

Lines changed: 91 additions & 16 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Added
2+
3+
- `Rel8.Record`, which adds experimental support for PostgreSQL's anonymous row types.

rel8.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ library
6767
Rel8.Expr.Num
6868
Rel8.Expr.Text
6969
Rel8.Expr.Time
70+
Rel8.Record
7071
Rel8.Tabulate
7172

7273
other-modules:

src/Rel8/Record.hs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{-# LANGUAGE DisambiguateRecordFields #-}
2+
{-# LANGUAGE FlexibleContexts #-}
3+
{-# LANGUAGE OverloadedStrings #-}
4+
{-# LANGUAGE ScopedTypeVariables #-}
5+
{-# LANGUAGE TypeApplications #-}
6+
{-# LANGUAGE UndecidableInstances #-}
7+
8+
module Rel8.Record (
9+
Record (Record),
10+
row,
11+
) where
12+
13+
-- base
14+
import Data.Functor.Contravariant ((>$<))
15+
import Prelude
16+
17+
-- opaleye
18+
import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
19+
20+
-- rel8
21+
import Rel8.Expr (Expr)
22+
import Rel8.Expr.Opaleye (fromPrimExpr, toPrimExpr)
23+
import Rel8.Schema.HTable (hfoldMap)
24+
import Rel8.Table (FromExprs, Table, fromResult, toColumns, toResult)
25+
import Rel8.Table.Eq (EqTable)
26+
import Rel8.Table.Ord (OrdTable)
27+
import Rel8.Type (DBType, typeInformation)
28+
import Rel8.Type.Composite (decodeComposite, encodeComposite)
29+
import Rel8.Type.Eq (DBEq)
30+
import Rel8.Type.Information (TypeInformation (TypeInformation))
31+
import Rel8.Type.Ord (DBOrd)
32+
import qualified Rel8.Type.Information
33+
34+
35+
{-| 'Record' is Rel8's support for PostgreSQL's anonymous record types. Any
36+
'Table' of 'Expr's can be converted to a 'Record' with 'row'.
37+
38+
Note that all of PostgreSQL's limitations on anonymous record types also
39+
apply to @Record@. For example, you won't be able to cast to 'Data.Text.Text'
40+
and back again like you can for other types. This also means that
41+
'Rel8.catListTable' will fail on nested 'Rel8.ListTable's that contain
42+
'Record's.
43+
-}
44+
newtype Record a = Record (FromExprs a)
45+
46+
47+
instance Table Expr a => DBType (Record a) where
48+
typeInformation =
49+
TypeInformation
50+
{ decode = Record . fromResult @_ @a <$> decodeComposite
51+
, encode = toResult @_ @a . (\(Record a) -> a) >$< encodeComposite
52+
, delimiter = ','
53+
, typeName = "record"
54+
}
55+
56+
57+
instance EqTable a => DBEq (Record a)
58+
59+
60+
instance OrdTable a => DBOrd (Record a)
61+
62+
63+
-- | Convert a 'Table' of 'Expr's to a single anonymous record 'Expr'.
64+
row :: Table Expr a => a -> Expr (Record a)
65+
row = fromPrimExpr . Opaleye.FunExpr "ROW" . hfoldMap (pure . toPrimExpr) . toColumns

src/Rel8/Type/Composite.hs

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module Rel8.Type.Composite
1818
( Composite( Composite )
1919
, DBComposite( compositeFields, compositeTypeName )
2020
, compose, decompose
21+
, decodeComposite, encodeComposite
2122
)
2223
where
2324

@@ -52,7 +53,7 @@ import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
5253
-- rel8
5354
import Rel8.Expr ( Expr )
5455
import Rel8.Expr.Opaleye ( castExpr, fromPrimExpr, toPrimExpr )
55-
import Rel8.Schema.HTable ( HTable, hfield, hspecs, htabulate, htabulateA )
56+
import Rel8.Schema.HTable (HTable, hfield, hfoldMap, hspecs, htabulate, htabulateA)
5657
import Rel8.Schema.Name ( Name( Name ) )
5758
import Rel8.Schema.Null ( Nullity( Null, NotNull ) )
5859
import Rel8.Schema.QualifiedName (QualifiedName)
@@ -97,19 +98,27 @@ newtype Composite a = Composite
9798
}
9899

99100

101+
decodeComposite :: HTable t => Decoder (t Result)
102+
decodeComposite =
103+
Decoder
104+
{ binary = Decoders.composite decoder
105+
, text = parser
106+
}
107+
108+
109+
encodeComposite :: forall t. HTable t => Encoder (t Result)
110+
encodeComposite =
111+
Encoder
112+
{ binary = Encoders.composite (encoder @t)
113+
, text = builder
114+
, quote = quoter . litHTable
115+
}
116+
117+
100118
instance DBComposite a => DBType (Composite a) where
101119
typeInformation = TypeInformation
102-
{ decode =
103-
Decoder
104-
{ binary = Decoders.composite (Composite . fromResult @_ @(HKD a Expr) <$> decoder)
105-
, text = fmap (Composite . fromResult @_ @(HKD a Expr)) . parser
106-
}
107-
, encode =
108-
Encoder
109-
{ binary = Encoders.composite (toResult @_ @(HKD a Expr) . unComposite >$< encoder)
110-
, text = builder . toResult @_ @(HKD a Expr) . unComposite
111-
, quote = quoter . litHTable . toResult @_ @(HKD a Expr) . unComposite
112-
}
120+
{ decode = Composite . fromResult @_ @(HKD a Expr) <$> decodeComposite
121+
, encode = toResult @_ @(HKD a Expr) . unComposite >$< encodeComposite
113122
, delimiter = ','
114123
, typeName =
115124
TypeName
@@ -256,7 +265,4 @@ buildRow elements =
256265

257266

258267
quoter :: HTable t => t Expr -> Opaleye.PrimExpr
259-
quoter a = Opaleye.FunExpr "ROW" exprs
260-
where
261-
exprs = getConst $ htabulateA \field -> case hfield a field of
262-
expr -> Const [toPrimExpr expr]
268+
quoter = Opaleye.FunExpr "ROW" . hfoldMap (pure . toPrimExpr)

0 commit comments

Comments
 (0)