1{-# language DeriveFunctor #-}
2{-# language DerivingStrategies #-}
3{-# language FlexibleContexts #-}
4{-# language TupleSections #-}
32import Control.Applicative ( liftA2 )
33import Control.Monad ( join, liftM2 )
34import Data.Bifunctor ( bimap, first )
35import Data.Foldable ( traverse_ )
36import Data.Maybe ( fromMaybe )
37import Prelude hiding ( filter, lookup, undefined, zip, zipWith )
40import Rel8.Aggregate ( Aggregate )
41import Rel8.Expr ( Expr )
42import Rel8.Expr.Bool ( not_, true )
43import Rel8.Query ( Query )
44import Rel8.Query.Aggregate ( aggregate )
45import Rel8.Query.Exists ( withBy, withoutBy )
46import Rel8.Query.Filter ( filter, where_ )
47import Rel8.Query.Maybe ( optional )
48import Rel8.Query.These ( alignBy )
49import Rel8.Table ( Table )
50import Rel8.Table.Aggregate ( groupBy )
51import Rel8.Table.Eq ( EqTable, (==:) )
52import Rel8.Table.Maybe ( MaybeTable )
53import Rel8.Table.These ( TheseTable, theseTable )
54import Rel8.Table.Undefined ( undefined )
57import Data.Functor.Apply ( Apply, liftF2 )
58import Data.Functor.Bind ( Bind )
59import qualified Data.Functor.Bind
60import Data.Semigroup.Traversable.Class ( bitraverse1 )
63-- | @'Tabulation' k a@ is denotionally a @MultiMap k a@ — a @Map@ where each
64-- key @k@ corresponds to potentially multiple @a@ (i.e., @'Query' a@). This
65-- @MultiMap@ supports 'lookup' and other operations you would expect it to.
67-- \"Identity\" 'Tabulation's are created using 'tabulate'. 'Tabulation's can
68-- be composed with 'Query's with 'prebind' or 'postbind' to form new
70newtype Tabulation k a = Tabulation (k -> Query (Maybe k, a))
71 deriving stock Functor
74instance EqTable k => Apply (Tabulation k) where
78instance EqTable k => Applicative (Tabulation k) where
79 pure = liftQuery . pure
83instance EqTable k => Bind (Tabulation k) where
87instance EqTable k => Monad (Tabulation k) where
88 Tabulation as >>= f = Tabulation $ \i -> do
91 Nothing -> case f a of
97 Nothing -> pure (mk, b)
103-- | This "undoes" 'fromQuery'.
106-- 'runTabulation' . 'fromQuery' = id
107-- 'fromQuery' . 'runTabulation' = id
110-- Note however that it produces nonsense when passed a 'Tabulation' that was
111-- made with from 'liftQuery' or 'pure'. In such cases the returned 'Query'
112-- will always produce zero rows.
113runTabulation :: EqTable k => Tabulation k a -> Query (k, a)
114runTabulation (Tabulation f) = do
118 -- Opaleye tries to be too clever optimising "WHERE FALSE" and generates
119 -- invalid SQL, so we use "WHERE NOT TRUE" instead
122 Just k -> pure (k, a)
127liftQuery :: Query a -> Tabulation k a
128liftQuery query = Tabulation $ const $ fmap (Nothing,) query
131-- | 'tabulate' creates an \"identity\" @'Tabulation' k a@ that allows @a@ be
132-- indexed by one or more of its columns @k@. Some examples:
134-- [Tabulation by primary key]:
136-- projectsById :: Project 'Expr' -> 'Tabulation' ('Expr' ProjectId) (Project 'Expr')
137-- projectsById = 'tabulate' projectId
140-- Note: the nature of primary keys means that each key will be mapped to a
141-- singleton value in this case.
143-- [Tabulation by other unique key]:
145-- projectsByName :: Project 'Expr' -> 'Tabulation' ('Expr' Text) (Project 'Expr')
146-- projectsByName = 'tabulate' projectName
149-- [Tabulation by foreign key (tabulate a child table by parent key)]:
151-- revisionsByProjectId :: Revision 'Expr' -> 'Tabulation' ('Expr' ProjectId) (Revision 'Expr')
152-- revisionsByProjectId = 'tabulate' revisionProjectId
154tabulate :: (a -> k) -> a -> Tabulation k a
155tabulate key a = fromQuery $ pure (key a, a)
158-- | Like 'tabulate' but takes a monadic 'Query' function instead of a pure
159-- one. This means you can filter rows while calculating the key, which is
160-- useful in conjunction with 'Rel8.Extra.catNulls'.
161tabulateA :: (a -> Query k) -> a -> Tabulation k a
162tabulateA key a = fromQuery $ (,a) <$> key a
165-- | Analgous to 'Data.Map.Strict.fromList'.
166fromQuery :: Query (k, a) -> Tabulation k a
167fromQuery = Tabulation . const . fmap (first Just)
170indexed :: Tabulation k a -> Tabulation k (k, a)
171indexed (Tabulation query) = Tabulation $ \i ->
172 (\(mk, a) -> (mk, (fromMaybe i mk, a))) <$> query i
175ifilter :: (k -> a -> Expr Bool) -> Tabulation k a -> Tabulation k a
176ifilter f tabulation = snd <$> do
177 filter (uncurry f) `postbind` indexed tabulation
180-- | Map a 'Query' over the input side of a 'Tabulation'.
181prebind :: (a -> Tabulation k b) -> Query a -> Tabulation k b
182prebind f as = Tabulation $ \k -> do
185 Tabulation query -> query k
189-- | Map a 'Query' over the output side of a 'Tabulation'.
190postbind :: (a -> Query b) -> Tabulation k a -> Tabulation k b
191postbind f (Tabulation as) = Tabulation $ \i -> do
200-- | Note that because 'Tabulation' is a @MultiMap@, the 'Query' returned by
201-- 'lookup' can and often does contain multiple results.
202lookup :: EqTable k => k -> Tabulation k a -> Query a
203lookup key (Tabulation query) = do
205 traverse_ (where_ . (key ==:)) mk
210-- [@align@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:align).
212-- If 'zip' makes an @INNER JOIN@, then 'align' makes a @FULL OUTER JOIN@.
213align :: (EqTable k, Table Expr a, Table Expr b)
214 => Tabulation k a -> Tabulation k b -> Tabulation k (TheseTable a b)
219-- [@alignWith@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:alignWith).
221-- See 'zipWith' and 'align'.
222alignWith :: (EqTable k, Table Expr a, Table Expr b)
223 => (TheseTable a b -> c)
224 -> Tabulation k a -> Tabulation k b -> Tabulation k c
225alignWith f kas kbs = do
229 tkab <- alignBy (\(l, _) (m, _) -> l ==: m) as bs
231 k' = theseTable fst fst (const fst) tkab
232 tab = bimap snd snd tkab
236-- | If 'zip' makes an @INNER JOIN@, then 'leftAlign' makes a @LEFT JOIN@.
237-- This means it will return at least one row for every row in the left
238-- 'Tabulation', even if there is no corresponding row in the right (hence the
239-- 'Rel8.MaybeTable').
242-- [@rpadZip@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:rpadZip).
243leftAlign :: EqTable k
244 => Tabulation k a -> Tabulation k b -> Tabulation k (a, MaybeTable b)
245leftAlign = leftAlignWith (,)
248-- | See 'zipWith' and 'leftAlign'.
251-- [@rpadZipWith@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:rpadZipWith).
252leftAlignWith :: EqTable k
253 => (a -> MaybeTable b -> c)
254 -> Tabulation k a -> Tabulation k b -> Tabulation k c
255leftAlignWith f left right = liftA2 f left (optionalTabulation right)
259-- [@zip@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:zip).
261-- There are multiple correct ways of understanding what this does.
263-- You can think of it as @'Data.Map.Strict.intersectionWith'
264-- ('Control.Applicative.liftA2' (,))@. That is, @intersect@ the two
265-- `Tabulation`s by matching their keys together (with 'Rel8.==:'), and combine
266-- their values (remembering that 'Tabulation' is a 'MultiMap' so that the
267-- values are keys) by getting their cartesian product.
269-- You can think of it as performing a cross product of the underlying 'Query's
270-- of the given 'Tabulation's and filtering the results for 'match'ing keys.
272-- You can think of it as a natural join in SQL terms.
274-- The size of the resulting 'Tabulation' will be \(\sum_{k} min(n_k, m_k) \)
275-- in terms of the number of keys, but \(\sum_{k} n_k \times m_k\) in terms of
276-- the number of values.
278 => Tabulation k a -> Tabulation k b -> Tabulation k (a, b)
283-- [@zipWith@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:zipWith).
287 => (a -> b -> c) -> Tabulation k a -> Tabulation k b -> Tabulation k c
291-- | 'similarity' returns all the entries in the left 'Tabulation' that have a
292-- corresponding entry in the right 'Tabulation'. This corresponds to a
293-- semijoin in relational algebra.
295-- This differs from @'zipWith' const x y@ when the right 'Tabulation' @y@
296-- contains an entry with multiple rows. For 'similarity', the entries in the
297-- resulting 'Tabulation' will contain the same number of rows as their
298-- respective entries in the left 'Tabulation' @x@. With `zipWith const x y`,
299-- each entry would contain the /product/ of the number of rows of their
300-- respective entries in @x@ and @y@.
303similarity :: EqTable k => Tabulation k a -> Tabulation k b -> Tabulation k a
304similarity kas kbs = do
307 fromQuery $ as >>= withBy (\(k, _) (l, _) -> k ==: l) bs
310-- | 'difference' returns all the entries in the left 'Tabulation' that don't
311-- exist in the right 'Tabulation'. This corresponds to an antijoin in
312-- relational algebra.
314-- See 'Rel8.without'.
315difference :: EqTable k => Tabulation k a -> Tabulation k b -> Tabulation k a
316difference kas kbs = do
319 fromQuery $ as >>= withoutBy (\(k, _) (l, _) -> k ==: l) bs
322aggregateTabulation :: EqTable k
323 => Tabulation k (Aggregate a) -> Tabulation k a
324aggregateTabulation kas = do
326 fromQuery $ aggregate $ bitraverse1 groupBy id <$> as
329optionalTabulation :: EqTable k
330 => Tabulation k a -> Tabulation k (MaybeTable a)
331optionalTabulation as = Tabulation $ \k -> do
332 ma <- optional $ lookup k as
336toQuery :: Tabulation k a -> Tabulation k (Query (k, a))
337toQuery (Tabulation as) = Tabulation $ \k ->
338 pure (Nothing, first (fromMaybe k) <$> as k)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list