on db40580 help sign in
rel8/src/Rel8/Tabulate.hs
406 lines 13 kB history raw
1{-# language DeriveFunctor #-}
2{-# language DerivingStrategies #-}
3{-# language FlexibleContexts #-}
4{-# language LambdaCase #-}
5{-# language TupleSections #-}
6{-# language UndecidableInstances #-}
8module Rel8.Tabulate
9 ( Tabulation
10 , runTabulation
11 , tabulate
12 , tabulateA
13 , fromQuery
14 , liftQuery
15 , prebind
16 , postbind
17 , indexed
18 , ifilter
19 , lookup
20 , align
21 , alignWith
22 , leftAlign
23 , leftAlignWith
24 , zip
25 , zipWith
26 , similarity
27 , difference
28 , aggregateTabulation
29 , orderTabulation
30 , singularize
31 , optionalTabulation
32 , manyTabulation
33 , someTabulation
34 )
35where
37-- base
38import Control.Applicative ( liftA2 )
39import Control.Monad ( join, liftM2 )
40import Data.Bifunctor ( bimap, first )
41import Data.Foldable ( traverse_ )
42import Data.Functor.Contravariant ( (>$<) )
43import Data.Maybe ( fromMaybe )
44import Prelude hiding ( filter, lookup, undefined, zip, zipWith )
46-- rel8
47import Rel8.Aggregate ( Aggregate )
48import Rel8.Expr ( Expr )
49import Rel8.Expr.Bool ( not_, true )
50import Rel8.Order ( Order )
51import Rel8.Query ( Query )
52import Rel8.Query.Aggregate ( aggregate )
53import Rel8.Query.Exists ( withBy, withoutBy )
54import Rel8.Query.Filter ( filter, where_ )
55import Rel8.Query.Maybe ( optional )
56import Rel8.Query.Order ( orderBy )
57import Rel8.Query.These ( alignBy )
58import Rel8.Table ( Table )
59import Rel8.Table.Aggregate ( groupBy, headAgg, listAgg, nonEmptyAgg )
60import Rel8.Table.Alternative
61 ( AltTable, (<|>:)
62 , AlternativeTable, emptyTable
63 )
64import Rel8.Table.Eq ( EqTable, (==:) )
65import Rel8.Table.List ( ListTable )
66import Rel8.Table.Maybe ( MaybeTable, maybeTable )
67import Rel8.Table.NonEmpty ( NonEmptyTable )
68import Rel8.Table.These ( TheseTable, theseTable )
69import Rel8.Table.Undefined ( undefined )
71-- semigroupoids
72import Data.Functor.Apply ( Apply, liftF2 )
73import Data.Functor.Bind ( Bind )
74import qualified Data.Functor.Bind
75import Data.Semigroup.Traversable.Class ( bitraverse1 )
78-- | @'Tabulation' k a@ is denotionally a @MultiMap k a@ — a @Map@ where each
79-- key @k@ corresponds to potentially multiple @a@ (i.e., @'Query' a@). This
80-- @MultiMap@ supports 'lookup' and other operations you would expect it to.
81--
82-- \"Identity\" 'Tabulation's are created using 'tabulate'. 'Tabulation's can
83-- be composed with 'Query's with 'prebind' or 'postbind' to form new
84-- 'Tabulation's.
85newtype Tabulation k a = Tabulation (Either k k -> Query (Maybe k, a))
86 deriving stock Functor
89instance EqTable k => Apply (Tabulation k) where
90 liftF2 = liftA2
93instance EqTable k => Applicative (Tabulation k) where
94 pure = liftQuery . pure
95 liftA2 = liftM2
98instance EqTable k => Bind (Tabulation k) where
99 join = join
102instance EqTable k => Monad (Tabulation k) where
103 Tabulation as >>= f = Tabulation $ \i -> do
104 (mk, a) <- as i
105 case mk of
106 Nothing -> case f a of
107 Tabulation bs -> bs i
108 Just k -> case f a of
109 Tabulation bs -> do
110 (mk', b) <- bs (Right k)
111 case mk' of
112 Nothing -> pure (mk, b)
113 Just k' -> do
114 where_ $ k ==: k'
115 pure (mk', b)
118instance EqTable k => AltTable (Tabulation k) where
119 kas <|>: kbs = do
120 as <- toQuery kas
121 bs <- toQuery kbs
122 fromQuery $ as <|>: bs
125instance EqTable k => AlternativeTable (Tabulation k) where
126 emptyTable = fromQuery emptyTable
129instance (EqTable k, Table Expr a, Semigroup a) => Semigroup (Tabulation k a)
130 where
131 (<>) = alignWith (theseTable id id (<>))
134instance (EqTable k, Table Expr a, Semigroup a) => Monoid (Tabulation k a)
135 where
136 mempty = emptyTable
139-- | This "undoes" 'fromQuery'.
140--
141-- @
142-- 'runTabulation' . 'fromQuery' = id
143-- 'fromQuery' . 'runTabulation' = id
144-- @
145--
146-- Note however that it produces nonsense when passed a 'Tabulation' that was
147-- made with from 'liftQuery' or 'pure'. In such cases the returned 'Query'
148-- will always produce zero rows.
149runTabulation :: EqTable k => Tabulation k a -> Query (k, a)
150runTabulation (Tabulation f) = do
151 (mk, a) <- f (Left undefined)
152 case mk of
153 Nothing -> do
154 -- Opaleye tries to be too clever optimising "WHERE FALSE" and generates
155 -- invalid SQL, so we use "WHERE NOT TRUE" instead
156 where_ $ not_ true
157 pure (undefined, a)
158 Just k -> pure (k, a)
161liftQuery :: Query a -> Tabulation k a
162liftQuery query = Tabulation $ const $ fmap (Nothing,) query
165-- | 'tabulate' creates an \"identity\" @'Tabulation' k a@ that allows @a@ be
166-- indexed by one or more of its columns @k@. Some examples:
167--
168-- [Tabulation by primary key]:
169-- @
170-- projectsById :: Project 'Expr' -> 'Tabulation' ('Expr' ProjectId) (Project 'Expr')
171-- projectsById = 'tabulate' projectId
172-- @
173--
174-- Note: the nature of primary keys means that each key will be mapped to a
175-- singleton value in this case.
176--
177-- [Tabulation by other unique key]:
178-- @
179-- projectsByName :: Project 'Expr' -> 'Tabulation' ('Expr' Text) (Project 'Expr')
180-- projectsByName = 'tabulate' projectName
181-- @
182--
183-- [Tabulation by foreign key (tabulate a child table by parent key)]:
184-- @
185-- revisionsByProjectId :: Revision 'Expr' -> 'Tabulation' ('Expr' ProjectId) (Revision 'Expr')
186-- revisionsByProjectId = 'tabulate' revisionProjectId
187-- @
188tabulate :: (a -> k) -> a -> Tabulation k a
189tabulate key a = fromQuery $ pure (key a, a)
192-- | Like 'tabulate' but takes a monadic 'Query' function instead of a pure
193-- one. This means you can filter rows while calculating the key, which is
194-- useful in conjunction with 'Rel8.Extra.catNulls'.
195tabulateA :: (a -> Query k) -> a -> Tabulation k a
196tabulateA key a = fromQuery $ (,a) <$> key a
199-- | Analgous to 'Data.Map.Strict.fromList'.
200fromQuery :: Query (k, a) -> Tabulation k a
201fromQuery = Tabulation . const . fmap (first Just)
204indexed :: Tabulation k a -> Tabulation k (k, a)
205indexed (Tabulation query) = Tabulation $ \i ->
206 (\(mk, a) -> (mk, (fromMaybe (either id id i) mk, a))) <$> query i
209ifilter :: (k -> a -> Expr Bool) -> Tabulation k a -> Tabulation k a
210ifilter f tabulation = snd <$> do
211 filter (uncurry f) `postbind` indexed tabulation
214-- | Map a 'Query' over the input side of a 'Tabulation'.
215prebind :: (a -> Tabulation k b) -> Query a -> Tabulation k b
216prebind f as = Tabulation $ \k -> do
217 a <- as
218 case f a of
219 Tabulation query -> query k
220infixr 1 `prebind`
223-- | Map a 'Query' over the output side of a 'Tabulation'.
224postbind :: (a -> Query b) -> Tabulation k a -> Tabulation k b
225postbind f (Tabulation as) = Tabulation $ \i -> do
226 (k, a) <- as i
227 case f a of
228 bs -> do
229 b <- bs
230 pure (k, b)
231infixr 1 `postbind`
234-- | Note that because 'Tabulation' is a @MultiMap@, the 'Query' returned by
235-- 'lookup' can and often does contain multiple results.
236lookup :: EqTable k => k -> Tabulation k a -> Query a
237lookup key (Tabulation query) = do
238 (mk, a) <- query (Right key)
239 traverse_ (where_ . (key ==:)) mk
240 pure a
243-- | Analagous to
244-- [@align@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:align).
245--
246-- If 'zip' makes an @INNER JOIN@, then 'align' makes a @FULL OUTER JOIN@.
247align :: (EqTable k, Table Expr a, Table Expr b)
248 => Tabulation k a -> Tabulation k b -> Tabulation k (TheseTable a b)
249align = alignWith id
252-- | Analagous to
253-- [@alignWith@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:alignWith).
254--
255-- See 'zipWith' and 'align'.
256alignWith :: (EqTable k, Table Expr a, Table Expr b)
257 => (TheseTable a b -> c)
258 -> Tabulation k a -> Tabulation k b -> Tabulation k c
259alignWith f kas kbs = do
260 as <- toQuery kas
261 bs <- toQuery kbs
262 fromQuery $ do
263 tkab <- alignBy (\(l, _) (m, _) -> l ==: m) as bs
264 let
265 k' = theseTable fst fst (const fst) tkab
266 tab = bimap snd snd tkab
267 pure (k', f tab)
270-- | If 'zip' makes an @INNER JOIN@, then 'leftAlign' makes a @LEFT JOIN@.
271-- This means it will return at least one row for every row in the left
272-- 'Tabulation', even if there is no corresponding row in the right (hence the
273-- 'Rel8.MaybeTable').
274--
275-- Analagous to
276-- [@rpadZip@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:rpadZip).
277leftAlign :: EqTable k
278 => Tabulation k a -> Tabulation k b -> Tabulation k (a, MaybeTable b)
279leftAlign = leftAlignWith (,)
282-- | See 'zipWith' and 'leftAlign'.
283--
284-- Analagous to
285-- [@rpadZipWith@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:rpadZipWith).
286leftAlignWith :: EqTable k
287 => (a -> MaybeTable b -> c)
288 -> Tabulation k a -> Tabulation k b -> Tabulation k c
289leftAlignWith f left right = liftA2 f left (optionalTabulation right)
292-- | Analagous to
293-- [@zip@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:zip).
294--
295-- There are multiple correct ways of understanding what this does.
296--
297-- You can think of it as @'Data.Map.Strict.intersectionWith'
298-- ('Control.Applicative.liftA2' (,))@. That is, @intersect@ the two
299-- `Tabulation`s by matching their keys together (with 'Rel8.==:'), and combine
300-- their values (remembering that 'Tabulation' is a 'MultiMap' so that the
301-- values are keys) by getting their cartesian product.
302--
303-- You can think of it as performing a cross product of the underlying 'Query's
304-- of the given 'Tabulation's and filtering the results for 'match'ing keys.
305--
306-- You can think of it as a natural join in SQL terms.
307--
308-- The size of the resulting 'Tabulation' will be \(\sum_{k} min(n_k, m_k) \)
309-- in terms of the number of keys, but \(\sum_{k} n_k \times m_k\) in terms of
310-- the number of values.
311zip :: EqTable k
312 => Tabulation k a -> Tabulation k b -> Tabulation k (a, b)
313zip = zipWith (,)
316-- | Analagous to
317-- [@zipWith@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:zipWith).
318--
319-- See 'zip'.
320zipWith :: EqTable k
321 => (a -> b -> c) -> Tabulation k a -> Tabulation k b -> Tabulation k c
322zipWith = liftA2
325-- | 'similarity' returns all the entries in the left 'Tabulation' that have a
326-- corresponding entry in the right 'Tabulation'. This corresponds to a
327-- semijoin in relational algebra.
328--
329-- This differs from @'zipWith' const x y@ when the right 'Tabulation' @y@
330-- contains an entry with multiple rows. For 'similarity', the entries in the
331-- resulting 'Tabulation' will contain the same number of rows as their
332-- respective entries in the left 'Tabulation' @x@. With `zipWith const x y`,
333-- each entry would contain the /product/ of the number of rows of their
334-- respective entries in @x@ and @y@.
335--
336-- See 'Rel8.with'.
337similarity :: EqTable k => Tabulation k a -> Tabulation k b -> Tabulation k a
338similarity kas kbs = do
339 as <- toQuery kas
340 bs <- toQuery kbs
341 fromQuery $ as >>= withBy (\(k, _) (l, _) -> k ==: l) bs
344-- | 'difference' returns all the entries in the left 'Tabulation' that don't
345-- exist in the right 'Tabulation'. This corresponds to an antijoin in
346-- relational algebra.
347--
348-- See 'Rel8.without'.
349difference :: EqTable k => Tabulation k a -> Tabulation k b -> Tabulation k a
350difference kas kbs = do
351 as <- toQuery kas
352 bs <- toQuery kbs
353 fromQuery $ as >>= withoutBy (\(k, _) (l, _) -> k ==: l) bs
356aggregateTabulation :: EqTable k
357 => Tabulation k (Aggregate a) -> Tabulation k a
358aggregateTabulation kas = do
359 as <- toQuery kas
360 fromQuery $ aggregate $ bitraverse1 groupBy id <$> as
363-- | 'orderTabulation' orders the /values/ of a 'Tabulation' (not the keys).
364-- In general this is meaningless, but if used together with 'manyTabulation'
365-- or 'someTabulation', the resulting lists will be ordered according to
366-- ordering given to 'orderTabulation'.
367orderTabulation :: Order a -> Tabulation k a -> Tabulation k a
368orderTabulation ordering (Tabulation as) =
369 Tabulation $ orderBy (snd >$< ordering) . as
372-- | Turns the given 'Tabulation' from a \"multimap\" into a \"map\". If there
373-- is more than one value at a particular key, only the first one is kept.
374-- \"First\" is in general undefined, but 'orderTabulation' can be used to
375-- make it deterministic.
376singularize :: (EqTable k, Table Expr a) => Tabulation k a -> Tabulation k a
377singularize = aggregateTabulation . fmap headAgg
380optionalTabulation :: EqTable k
381 => Tabulation k a -> Tabulation k (MaybeTable a)
382optionalTabulation as = Tabulation $ \case
383 Left k -> case as of
384 Tabulation f -> fmap pure <$> f (Left k)
385 Right k -> do
386 ma <- optional $ lookup k as
387 pure (Just k, ma)
390manyTabulation :: (EqTable k, Table Expr a)
391 => Tabulation k a -> Tabulation k (ListTable a)
392manyTabulation =
393 fmap (maybeTable mempty id) .
394 optionalTabulation .
395 aggregateTabulation .
396 fmap listAgg
399someTabulation :: (EqTable k, Table Expr a)
400 => Tabulation k a -> Tabulation k (NonEmptyTable a)
401someTabulation = aggregateTabulation . fmap nonEmptyAgg
404toQuery :: Tabulation k a -> Tabulation k (Query (k, a))
405toQuery (Tabulation as) = Tabulation $ \k ->
406 pure (Nothing, first (fromMaybe (either id id k)) <$> as k)