on 531d21c help sign in
rel8/src/Rel8/Query/Maybe.hs
88 lines 3.3 kB history raw
1module Rel8.Query.Maybe
2 ( optional
3 , catMaybeTable
4 , bindMaybeTable
5 , traverseMaybeTable
6 )
7where
9-- base
10import Data.Functor ( (<&>) )
11import Prelude
13-- opaleye
14import qualified Opaleye.Internal.PackMap as Opaleye
15import qualified Opaleye.Internal.PrimQuery as Opaleye
16import qualified Opaleye.Internal.QueryArr as Opaleye
17import qualified Opaleye.Internal.Tag as Opaleye
18import qualified Opaleye.Internal.Unpackspec as Opaleye
20-- rel8
21import Rel8.Expr.Bool ( true )
22import Rel8.Expr.Eq ( (==.) )
23import Rel8.Expr.Opaleye ( toPrimExpr )
24import Rel8.Query ( Query )
25import Rel8.Query.Filter ( where_ )
26import Rel8.Query.Opaleye ( mapOpaleye )
27import Rel8.Table.Maybe ( MaybeTable( MaybeTable ), isJustTable )
28import Rel8.Table.Opaleye ( unpackspec )
29import Rel8.Table.Tag ( Tag(..), fromExpr )
32-- | Convert a query that might return zero rows to a query that always returns
33-- at least one row.
34--
35-- To speak in more concrete terms, 'optional' is most useful to write @LEFT
36-- JOIN@s.
37optional :: Query a -> Query (MaybeTable a)
38optional = mapOpaleye $ Opaleye.QueryArr . go
39 where
40 go query (i, left, tag) =
41 (MaybeTable (fromExpr t') a, join, Opaleye.next tag')
42 where
43 (MaybeTable Tag {expr = t} a, right, tag') =
44 Opaleye.runSimpleQueryArr (pure <$> query) (i, tag)
45 (t', bindings) = Opaleye.run $
46 Opaleye.runUnpackspec unpackspec (Opaleye.extractAttr "maybe" tag') t
47 join = Opaleye.Join Opaleye.LeftJoin condition [] bindings left right
48 condition = toPrimExpr true
51-- | Filter out 'MaybeTable's, returning only the tables that are not-null.
52--
53-- This operation can be used to "undo" the effect of 'optional', which
54-- operationally is like turning a @LEFT JOIN@ back into a full @JOIN@. You
55-- can think of this as analogous to 'Data.Maybe.catMaybes'.
56catMaybeTable :: MaybeTable a -> Query a
57catMaybeTable ma@(MaybeTable _ a) = do
58 where_ $ isJustTable ma
59 pure a
62-- | @bindMaybeTable f x@ is similar to the monadic bind (@>>=@) operation. It
63-- allows you to "extend" an optional query with another query. If either the
64-- input or output are 'Rel8.nothingTable', then the result is
65-- 'Rel8.nothingTable'.
66--
67-- This is similar to 'traverseMaybeTable', followed by a @join@ on the
68-- resulting @MaybeTable@s.
69bindMaybeTable :: Functor m
70 => (a -> m (MaybeTable b)) -> MaybeTable a -> m (MaybeTable b)
71bindMaybeTable query (MaybeTable input a) =
72 query a <&> \(MaybeTable output b) -> MaybeTable (input <> output) b
75-- | Extend an optional query with another query. This is useful if you want
76-- to step through multiple @LEFT JOINs@.
77--
78-- Note that @traverseMaybeTable@ takes a @a -> Query b@ function, which means
79-- you also have the ability to "expand" one row into multiple rows. If the
80-- @a -> Query b@ function returns no rows, then the resulting query will also
81-- have no rows. However, regardless of the given @a -> Query b@ function, if
82-- the input is @nothingTable@, you will always get exactly one @nothingTable@
83-- back.
84traverseMaybeTable :: (a -> Query b) -> MaybeTable a -> Query (MaybeTable b)
85traverseMaybeTable query ma@(MaybeTable input _) = do
86 MaybeTable output b <- optional (query =<< catMaybeTable ma)
87 where_ $ expr output ==. expr input
88 pure $ MaybeTable input b