Skip to content

Commit f68d83b

Browse files
Add support for PostgreSQL's anonymous row types
1 parent 80a4449 commit f68d83b

File tree

5 files changed

+122
-17
lines changed

5 files changed

+122
-17
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
### Added
2+
3+
- `Rel8.Record`, which adds experimental support for PostgreSQL's anonymous row types.
4+
- Add `elem` and `elem1` to `Rel8.Array` for testing if an element is contained in `[]` and `NonEmpty` `Expr`s.

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/Array.hs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
{-# LANGUAGE FlexibleContexts #-}
2+
{-# LANGUAGE MonoLocalBinds #-}
3+
{-# LANGUAGE OverloadedStrings #-}
4+
15
module Rel8.Array
26
(
37
-- ** @ListTable@
@@ -6,13 +10,15 @@ module Rel8.Array
610
, index, indexExpr
711
, last, lastExpr
812
, length, lengthExpr
13+
, elem
914

1015
-- ** @NonEmptyTable@
1116
, NonEmptyTable
1217
, head1, head1Expr
1318
, index1, index1Expr
1419
, last1, last1Expr
1520
, length1, length1Expr
21+
, elem1
1622

1723
-- ** Unsafe
1824
, unsafeSubscript
@@ -21,11 +27,34 @@ module Rel8.Array
2127
where
2228

2329
-- base
24-
import Prelude hiding (head, last, length)
30+
import Data.List.NonEmpty (NonEmpty)
31+
import Prelude hiding (elem, head, last, length)
2532

2633
-- rel8
34+
import Rel8.Expr (Expr)
35+
import Rel8.Expr.Array (listOf, nonEmptyOf)
36+
import Rel8.Expr.Function (rawBinaryOperator)
2737
import Rel8.Expr.List
2838
import Rel8.Expr.NonEmpty
2939
import Rel8.Expr.Subscript
40+
import Rel8.Schema.Null (Sql)
3041
import Rel8.Table.List
3142
import Rel8.Table.NonEmpty
43+
import Rel8.Type.Eq (DBEq)
44+
45+
46+
-- | @'elem' a as@ tests whether @a@ is an element of the list @as@.
47+
elem :: Sql DBEq a => Expr a -> Expr [a] -> Expr Bool
48+
elem = (<@) . listOf . pure
49+
where
50+
(<@) = rawBinaryOperator "<@"
51+
infix 4 `elem`
52+
53+
54+
-- | @'elem1' a as@ tests whether @a@ is an element of the non-empty list
55+
-- @as@.
56+
elem1 :: Sql DBEq a => Expr a -> Expr (NonEmpty a) -> Expr Bool
57+
elem1 = (<@) . nonEmptyOf . pure
58+
where
59+
(<@) = rawBinaryOperator "<@"
60+
infix 4 `elem1`

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)