on dec2074 help sign in
rel8/src/Rel8/Query/Maybe.hs
136 lines 4.9 kB history raw
1module Rel8.Query.Maybe
2 ( optional
3 , catMaybeTable
4 , bindMaybeTable
5 , traverseMaybeTable
6 )
7where
9-- base
10import Prelude
12-- opaleye
13import qualified Opaleye.Internal.PackMap as Opaleye
14import qualified Opaleye.Internal.PrimQuery as Opaleye
15import qualified Opaleye.Internal.QueryArr as Opaleye
16import qualified Opaleye.Internal.Tag as Opaleye
17import qualified Opaleye.Internal.Unpackspec as Opaleye
19-- rel8
20import Rel8.Expr.Bool ( true )
21import Rel8.Expr.Eq ( (==.) )
22import Rel8.Expr.Opaleye ( toPrimExpr )
23import Rel8.Query ( Query )
24import Rel8.Query.Filter ( where_ )
25import Rel8.Query.Opaleye ( mapOpaleye )
26import Rel8.Table.Maybe ( MaybeTable( MaybeTable ), isJustTable )
27import Rel8.Table.Opaleye ( unpackspec )
30optional :: Query a -> Query (MaybeTable a)
31optional = mapOpaleye $ Opaleye.QueryArr . go
32 where
33 go query (i, left, tag) = (MaybeTable t' a, join, Opaleye.next tag')
34 where
35 (MaybeTable t a, right, tag') =
36 Opaleye.runSimpleQueryArr (pure <$> query) (i, tag)
37 (t', bindings) = Opaleye.run $
38 Opaleye.runUnpackspec unpackspec (Opaleye.extractAttr "maybe" tag') t
39 join = Opaleye.Join Opaleye.LeftJoin condition [] bindings left right
40 condition = toPrimExpr true
43-- | Filter out 'MaybeTable's, returning only the tables that are not-null.
44--
45-- This operation can be used to "undo" the effect of 'optional', which
46-- operationally is like turning a @LEFT JOIN@ back into a full @JOIN@. You
47-- can think of this as analogous to 'Data.Maybe.catMaybes'.
48--
49-- To see this in action, first consider the following 'optional' query:
50--
51-- >>> :{
52-- select c $ do
53-- author <- each authorSchema
54-- maybeRel8 <- optional $
55-- each projectSchema
56-- >>= filter (\p -> projectAuthorId p ==. authorId author)
57-- >>= filter (\p -> projectName p ==. "rel8")
58-- return (authorName author, projectName <$> maybeRel8)
59-- :}
60-- [("Ollie",Just "rel8"),("Bryan O'Sullivan",Nothing),("Emily Pillmore",Nothing)]
61--
62-- Here @optional@ is acting as a @LEFT JOIN@. We can turn this into a proper
63-- join by using @catMaybeTable@ to filter out rows where the join failed:
64--
65-- >>> :{
66-- select c $ do
67-- author <- each authorSchema
68-- maybeRel8 <- optional $
69-- each projectSchema
70-- >>= filter (\p -> projectAuthorId p ==. authorId author)
71-- >>= filter (\p -> projectName p ==. "rel8")
72-- rel8 <- catMaybeTable maybeRel8
73-- return (authorName author, projectName rel8)
74-- :}
75-- [("Ollie","rel8")]
76catMaybeTable :: MaybeTable a -> Query a
77catMaybeTable ma@(MaybeTable _ a) = do
78 where_ $ isJustTable ma
79 pure a
82-- | @bindMaybeTable f x@ is similar to the monadic bind (@>>=@) operation. It
83-- allows you to "extend" an optional query with another query. If either the
84-- input or output are 'noTable', then the result is 'noTable'.
85--
86-- This is similar to 'traverseMaybeTable', followed by a @join@ on the
87-- resulting @MaybeTable@s.
88--
89-- >>> select c $ bindMaybeTable (optional . values . pure . not_) =<< optional (values [lit True])
90-- [Just False]
91--
92-- >>> select c $ bindMaybeTable (\_ -> return (noTable :: MaybeTable (Expr Bool))) =<< optional (values [lit True])
93-- [Nothing]
94--
95-- >>> select c $ bindMaybeTable (optional . values . pure . not_) =<< return (noTable :: MaybeTable (Expr Bool))
96-- [Nothing]
97bindMaybeTable :: Monad m
98 => (a -> m (MaybeTable b)) -> MaybeTable a -> m (MaybeTable b)
99bindMaybeTable query (MaybeTable input a) = do
100 MaybeTable output b <- query a
101 pure $ MaybeTable (input <> output) b
104-- | Extend an optional query with another query. This is useful if you want
105-- to step through multiple @LEFT JOINs@.
106--
107-- Note that @traverseMaybeTable@ takes a @a -> Query b@ function, which means
108-- you also have the ability to "expand" one row into multiple rows.
109--
110-- >>> :{
111-- duplicate :: Expr Bool -> Query (Expr Bool)
112-- duplicate x = unionAll (return x) (return x)
113-- :}
114--
115-- >>> select c $ traverseMaybeTable duplicate =<< optional (values [lit True])
116-- [Just True,Just True]
117--
118-- Note that if the @a -> Query b@ function returns no rows, then the resulting
119-- query will also have no rows:
120--
121-- >>> select c $ traverseMaybeTable (limit 0 . pure) =<< optional (values [lit True])
122-- []
123--
124-- However, regardless of the given @a -> Query b@ function, if the input is
125-- @noTable@, you will always get exactly one @noTable@ back:
126--
127-- >>> select c $ traverseMaybeTable duplicate (noTable :: MaybeTable (Expr Bool))
128-- [Nothing]
129--
130-- >>> select c $ traverseMaybeTable (limit 0 . pure) (noTable :: MaybeTable (Expr Bool))
131-- [Nothing]
132traverseMaybeTable :: (a -> Query b) -> MaybeTable a -> Query (MaybeTable b)
133traverseMaybeTable query ma@(MaybeTable input _) = do
134 MaybeTable output b <- optional (query =<< catMaybeTable ma)
135 where_ $ output ==. input
136 pure $ MaybeTable input b