@@ -1,254 +1,131 @@
1+{-# language DeriveFunctor #-}
2+{-# language DerivingStrategies #-}
13{-# language FlexibleContexts #-}
2−{-# language MonoLocalBinds #-}
3−{-# language ScopedTypeVariables #-}
4−{-# language StandaloneKindSignatures #-}
5−{-# language TypeApplications #-}
64{-# language TupleSections #-}
75{-# language UndecidableInstances #-}
86
9−-- | "Rel8.Tabulate" provides an alternative API ('Tabulation') for writing
10−-- queries that complements the main "Rel8" API ('Query').
11−
127module Rel8.Tabulate
13− (
14− Tabulation
15−
16− -- * Interfacing with 'Query's
8+ ( Tabulation
9+ , tabulate
10+ , tabulateA
11+ , runTabulation
1712 , fromQuery
18− , toQuery
1913 , liftQuery
20− , through
14+ , prebind
15+ , postbind
16+ , indexed
17+ , ifilter
2118 , lookup
22−
23− -- * Aggregation and Ordering
24− , aggregate
25− , distinct
26− , order
27−
28− -- ** Magic 'Tabulation's
29− -- $magic
30− , count
31− , optional
32− , many
33− , some
34− , exists
35− , present
36− , absent
37−
38− -- * Natural joins
3919 , align
4020 , alignWith
4121 , leftAlign
4222 , leftAlignWith
43− , rightAlign
44− , rightAlignWith
4523 , zip
4624 , zipWith
4725 , similarity
4826 , difference
27+ , aggregateTabulation
28+ , orderTabulation
29+ , distinctTabulation
30+ , optionalTabulation
31+ , manyTabulation
32+ , someTabulation
4933 )
5034where
5135
5236-- base
53−import Control.Applicative ( (<|>), empty, liftA2 )
54−import Control.Monad ( liftM2 )
55−import Data.Bifunctor ( Bifunctor, bimap, first, second )
37+import Control.Applicative ( liftA2 )
38+import Control.Monad ( join, liftM2 )
39+import Data.Bifunctor ( bimap, first )
5640import Data.Foldable ( traverse_ )
57−import Data.Function ( on )
58−import Data.Functor.Contravariant ( Contravariant, (>$<), contramap )
59−import Data.Int ( Int64 )
60−import Data.Kind ( Type )
61−import Data.Maybe ( fromJust, fromMaybe )
62−import Prelude hiding ( lookup, zip, zipWith )
63−
64−-- bifunctors
65−import Data.Bifunctor.Clown ( Clown( Clown ), runClown )
66−
67−-- comonad
68−import Control.Comonad ( extract )
69−
70−-- opaleye
71−import qualified Opaleye.Aggregate as Opaleye
72−import qualified Opaleye.Order as Opaleye ( orderBy, distinctOnExplicit )
73−
74−-- profunctors
75−import Data.Profunctor ( dimap, lmap )
76−
77−-- product-profunctors
78−import Data.Profunctor.Product
79− ( ProductProfunctor, (***!)
80− , SumProfunctor, (+++!)
81− )
82−import qualified Data.Profunctor.Product as PP
41+import Data.Functor.Contravariant ( (>$<) )
42+import Data.Maybe ( fromMaybe )
43+import Prelude hiding ( filter, lookup, zip, zipWith )
8344
8445-- rel8
85−import Rel8.Aggregate ( Aggregates )
46+import Rel8.Aggregate ( Aggregate )
8647import Rel8.Expr ( Expr )
87−import Rel8.Expr.Aggregate ( countStar )
88−import Rel8.Expr.Bool ( true )
89−import Rel8.Order ( Order( Order ) )
48+import Rel8.Order ( Order )
9049import Rel8.Query ( Query )
91−import qualified Rel8.Query.Exists as Q ( exists, present, absent )
92−import Rel8.Query.Filter ( where_ )
93−import Rel8.Query.List ( catNonEmptyTable )
94−import qualified Rel8.Query.Maybe as Q ( optional )
95−import Rel8.Query.Opaleye ( mapOpaleye, unsafePeekQuery )
96−import Rel8.Query.Rebind ( rebind )
50+import Rel8.Query.Aggregate ( aggregate )
51+import Rel8.Query.Distinct ( distinctOn )
52+import Rel8.Query.Exists ( withBy, withoutBy )
53+import Rel8.Query.Filter ( filter, where_ )
54+import Rel8.Query.Maybe ( optional )
55+import Rel8.Query.Order ( orderBy )
9756import Rel8.Query.These ( alignBy )
98−import Rel8.Table ( Table, fromColumns, toColumns )
99−import Rel8.Table.Aggregate ( hgroupBy, listAgg, nonEmptyAgg )
57+import Rel8.Table ( Table )
58+import Rel8.Table.Aggregate ( groupBy, listAgg, nonEmptyAgg )
10059import Rel8.Table.Alternative
10160 ( AltTable, (<|>:)
10261 , AlternativeTable, emptyTable
10362 )
104−import Rel8.Table.Cols ( fromCols, toCols )
105−import Rel8.Table.Eq ( EqTable, (==:), eqTable )
106−import Rel8.Table.List ( ListTable( ListTable ) )
107−import Rel8.Table.Maybe ( MaybeTable( MaybeTable ), maybeTable )
108−import Rel8.Table.NonEmpty ( NonEmptyTable( NonEmptyTable ) )
109−import Rel8.Table.Opaleye ( aggregator, unpackspec )
63+import Rel8.Table.Eq ( EqTable, (==:) )
64+import Rel8.Table.List ( ListTable )
65+import Rel8.Table.Maybe ( MaybeTable, maybeTable )
66+import Rel8.Table.NonEmpty ( NonEmptyTable )
11067import Rel8.Table.Ord ( OrdTable )
11168import Rel8.Table.Order ( ascTable )
112−import Rel8.Table.Projection
113− ( Biprojectable, biproject
114− , Projectable, project
115− , apply
116− )
117−import Rel8.Table.These ( TheseTable( TheseTable ), theseTable )
69+import Rel8.Table.These ( TheseTable, theseTable )
11870
11971-- semigroupoids
12072import Data.Functor.Apply ( Apply, liftF2 )
121−import Data.Functor.Bind ( Bind, (>>-) )
122−
123−
124−type Key :: Type -> Type
125−type Key = Maybe
126−
127−
128−cat :: Table Expr k => Key k -> Query k
129−cat = maybe emptyTable pure
130−
131−
132−key :: (ProductProfunctor p, SumProfunctor p)
133− => p a b -> p (Key a) (Key b)
134−key a = dimap from to (PP.empty +++! a)
135− where
136− from = maybe (Left ()) Right
137− to = either (const Nothing) Just
138−
139−
140−keyed :: (ProductProfunctor p, SumProfunctor p)
141− => p k l -> p a b -> p (Key k, a) (Key l, b)
142−keyed k a = key k ***! a
143−
144−
145−type Predicate :: Type -> Type
146−newtype Predicate a = Predicate (Maybe (a -> Expr Bool))
147−
148−
149−instance Contravariant Predicate where
150− contramap f (Predicate a) = Predicate (lmap f <$> a)
151−
152−
153−instance Semigroup (Predicate k) where
154− Predicate ma <> Predicate mb = Predicate $ ma <|> mb
155−
156−
157−instance Monoid (Predicate k) where
158− mempty = Predicate Nothing
159−
160−
161−match :: EqTable k => Key k -> Predicate k
162−match = Predicate . fmap (==:)
163−
73+import Data.Functor.Bind ( Bind )
74+import qualified Data.Functor.Bind
75+import Data.Semigroup.Traversable.Class ( bitraverse1 )
16476
165−ensure :: Predicate k -> Key k -> Query ()
166−ensure (Predicate mp) = traverse_ (\k -> traverse_ (\p -> where_ (p k)) mp)
16777
168−
169−-- | A @'Tabulation' k a@ is like a @'Query' a@, except that each row also
170−-- has a key @k@ in addition to the value @a@. 'Tabulation's can be composed
171−-- monadically just like 'Query's, but the resulting join is more like a
172−-- @NATURAL JOIN@ (based on the common key column(s) @k@) than the
173−-- @CROSS JOIN@ given by 'Query'.
174−--
175−-- Another way to think of @'Tabulation' k a@ is as analogous to @Map k a@ in
176−-- the same way @'Query' a@ is analogous to @[a]@. However, there's nothing
177−-- stopping a 'Tabulation' from containing multiple rows with the same key, so
178−-- technically @Map k (NonEmpty a)@ is more accurate.
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.
17981--
180−-- 'Tabulation's can be created from 'Query's with 'fromQuery' and 'liftQuery'
181−-- and converted back to 'Query's with 'lookup' and 'toQuery' (though note the
182−-- caveats that come with the latter).
183−type Tabulation :: Type -> Type -> Type
184−newtype Tabulation k a = Tabulation (Predicate k -> Query (Key k, a))
185−
186−
187−instance Biprojectable Tabulation where
188− biproject f g =
189− bimap
190− (fromColumns . apply f . toColumns)
191− (fromColumns . apply g . toColumns)
192−
193−
194−instance Bifunctor Tabulation where
195− bimap f g (Tabulation a) = Tabulation $ \p ->
196− bimap (fmap f) g <$> a (f >$< p)
197−
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.
85+newtype Tabulation k a = Tabulation (k -> Query (Maybe k, a))
86+ deriving stock Functor
19887
199−instance Functor (Tabulation k) where
200− fmap = second
20188
202−
203−instance Projectable (Tabulation k) where
204− project f = fmap (fromColumns . apply f . toColumns)
205−
206−
207−-- | If @'Tabulation' k a@ is @Map k (NonEmpty a)@, then @(<.>)@ is
208−-- @intersectionWith (liftA2 (<*>))@
20989instance EqTable k => Apply (Tabulation k) where
21090 liftF2 = liftA2
21191
21292
213−-- | @pure = 'liftQuery' . pure@
21493instance EqTable k => Applicative (Tabulation k) where
21594 pure = liftQuery . pure
21695 liftA2 = liftM2
21796
21897
21998instance EqTable k => Bind (Tabulation k) where
220− Tabulation as >>- f = Tabulation $ \p -> do
221− (k, a) <- as p
222− case f a of
223− Tabulation bs -> do
224− let p' = match k
225− (k', b) <- bs (p' <> p)
226− ensure p' k'
227− pure (k' <|> k, b)
99+ join = join
228100
229101
230102instance EqTable k => Monad (Tabulation k) where
231− (>>=) = (>>-)
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 k
111+ case mk' of
112+ Nothing -> pure (mk, b)
113+ Just k' -> do
114+ where_ $ k ==: k'
115+ pure (mk', b)
232116
233117
234−-- | If @'Tabulation' k a@ is @Map k (NonEmpty a)@, then @(<|>:)@ is
235−-- @unionWith (<>)@.
236118instance EqTable k => AltTable (Tabulation k) where
237− tas <|>: tbs = do
238− eas <- peek tas
239− ebs <- peek tbs
240− case (eas, ebs) of
241− (Left as, Left bs) -> liftQuery $ as <|>: bs
242− (Right as, Right bs) -> fromQuery $ as <|>: bs
243− _ -> catNonEmptyTable `through` ((<>) `on` some) tas tbs
119+ kas <|>: kbs = do
120+ as <- toQuery kas
121+ bs <- toQuery kbs
122+ fromQuery $ as <|>: bs
244123
245124
246125instance EqTable k => AlternativeTable (Tabulation k) where
247− emptyTable = Tabulation $ const $ fmap (empty,) emptyTable
126+ emptyTable = fromQuery emptyTable
248127
249128
250−-- | If @'Tabulation' k a@ is @Map k (NonEmpty a)@, then @(<>)@ is
251−-- @unionWith (liftA2 (<>))@.
252129instance (EqTable k, Table Expr a, Semigroup a) => Semigroup (Tabulation k a)
253130 where
254131 (<>) = alignWith (theseTable id id (<>))
@@ -259,382 +136,258 @@ instance (EqTable k, Table Expr a, Semigroup a) => Monoid (Tabulation k a)
259136 mempty = emptyTable
260137
261138
262−-- | Any 'Query' of key-value pairs @(k, a)@ can be a @'Tabulation' k a@.
263−fromQuery :: Query (k, a) -> Tabulation k a
264−fromQuery = Tabulation . const . fmap (first pure)
139+runTabulation :: EqTable k => Query k -> Tabulation k a -> Query (k, a)
140+runTabulation ks tabulation = do
141+ k <- ks
142+ a <- lookup k tabulation
143+ pure (k, a)
265144
266145
267−-- | Convert a @'Tabulation' k a@ back into a 'Query' of key-value pairs.
268−--
269−-- Note that the result of a 'toQuery' is undefined (will always return zero
270−-- rows) on 'Tabulation's constructed with 'liftQuery' or 'pure'. So while
271−-- @toQuery . fromQuery@ is always @id@, @fromQuery . toQuery@ is not.
146+liftQuery :: Query a -> Tabulation k a
147+liftQuery query = Tabulation $ const $ fmap (Nothing,) query
148+
149+
150+-- | 'tabulate' creates an \"identity\" @'Tabulation' k a@ that allows @a@ be
151+-- indexed by one or more of its columns @k@. Some examples:
272152--
273−-- A safer, more predictable alternative to 'toQuery' is to use 'lookup' with
274−-- an explicit set of keys:
153+-- [Tabulation by primary key]:
154+-- @
155+-- projectsById :: Project 'Expr' -> 'Tabulation' ('Expr' ProjectId) (Project 'Expr')
156+-- projectsById = 'tabulate' projectId
157+-- @
275158--
276−-- @
277−-- do
278−-- k <- keys
279−-- a <- lookup k tabulation
280−-- pure (k, a)
281−-- @
159+-- Note: the nature of primary keys means that each key will be mapped to a
160+-- singleton value in this case.
282161--
283−-- Having said that, in practice, most legitimate uses of 'Tabulation' will
284−-- have a well-defined 'toQuery'. It would be possible in theory to encode
285−-- the necessary invariants at the type level using an indexed monad, but we
286−-- would lose the ability to use @do@-notation, which is the main benefit
287−-- of having 'Tabulation' as a monad in the first place.
162+-- [Tabulation by other unique key]:
163+-- @
164+-- projectsByName :: Project 'Expr' -> 'Tabulation' ('Expr' Text) (Project 'Expr')
165+-- projectsByName = 'tabulate' projectName
166+-- @
288167--
289−-- In particular, @'toQuery' t@ is well-defined for any 'Tabulation' @t@
290−-- defined as @t = fromQuery _@. @'toQuery' t@ is also well-defined for any
291−-- 'Tabulation' @t@ defined as @t = t' >>= _@ or @t = t' *> _@ where
292−-- @'toQuery' t'@ is well-defined. There are other valid permutations too.
293−-- Generally, anything that uses 'fromQuery' at some point, unless wrapped in
294−-- a top-level 'present' or 'absent', will have a well-defined 'toQuery'.
295−toQuery :: Table Expr k => Tabulation k a -> Query (k, a)
296−toQuery (Tabulation f) = do
297− (mk, a) <- f mempty
298− k <- cat mk
299− pure (k, a)
300−
301−
302−-- | A @'Query' a@ can be treated as a @'Tabulation' k a@ where the given @a@
303−-- values exist at every possible key @k@.
304−liftQuery :: Query a -> Tabulation k a
305−liftQuery = Tabulation . const . fmap (empty,)
168+-- [Tabulation by foreign key (tabulate a child table by parent key)]:
169+-- @
170+-- revisionsByProjectId :: Revision 'Expr' -> 'Tabulation' ('Expr' ProjectId) (Revision 'Expr')
171+-- revisionsByProjectId = 'tabulate' revisionProjectId
172+-- @
173+tabulate :: (a -> k) -> a -> Tabulation k a
174+tabulate key a = fromQuery $ pure (key a, a)
306175
307176
308−-- | Run a Kleisli arrow in the the 'Query' monad \"through\" a 'Tabulation'.
309−-- Useful for 'Rel8.filter'ing a 'Tabulation'.
310−--
311−-- @
312−-- 'Rel8.filter' ((>=. 30) . userAge) `'through'` usersById
313−-- @
314−through :: (a -> Query b) -> Tabulation k a -> Tabulation k b
315−through f (Tabulation as) = Tabulation $ \p -> do
316− (k, a) <- as p
317− b <- f a
318− pure (k, b)
319−infixr 1 `through`
320−
321−
322−-- | @'lookup' k t@ returns the value(s) at the key @k@ in the tabulation @t@.
323−lookup :: EqTable k => k -> Tabulation k a -> Query a
324−lookup k (Tabulation f) = do
325− (k', a) <- f p
326− ensure p k'
327− pure a
328− where
329− p = match (pure k)
177+-- | Like 'tabulate' but takes a monadic 'Query' function instead of a pure
178+-- one. This means you can filter rows while calculating the key, which is
179+-- useful in conjunction with 'Rel8.Extra.catNulls'.
180+tabulateA :: (a -> Query k) -> a -> Tabulation k a
181+tabulateA key a = fromQuery $ (,a) <$> key a
330182
331183
332−-- | 'aggregate' aggregates the values within each key of a
333−-- 'Tabulation'. There is an implicit @GROUP BY@ on all the key columns.
334−aggregate :: forall k aggregates exprs.
335− ( EqTable k
336− , Aggregates aggregates exprs
337− )
338− => Tabulation k aggregates -> Tabulation k exprs
339−aggregate (Tabulation f) = Tabulation $
340− mapOpaleye (Opaleye.aggregate (keyed haggregator aggregator)) .
341− fmap (first (fmap (hgroupBy (eqTable @k) . toColumns))) .
342− f
343− where
344− haggregator = dimap fromColumns fromCols aggregator
184+-- | Analgous to 'Data.Map.Strict.fromList'.
185+fromQuery :: Query (k, a) -> Tabulation k a
186+fromQuery = Tabulation . const . fmap (first Just)
345187
346188
347−-- | 'distinct' ensures a 'Tabulation' has at most one value for
348−-- each key, i.e., it drops duplicates. In general it keeps only the
349−-- \"first\" value it encounters for each key, but note that \"first\" is
350−-- undefined unless you first call 'order'.
351−distinct :: EqTable k => Tabulation k a -> Tabulation k a
352−distinct (Tabulation f) = Tabulation $
353− mapOpaleye (Opaleye.distinctOnExplicit (key unpackspec) fst) . f
189+indexed :: Tabulation k a -> Tabulation k (k, a)
190+indexed (Tabulation query) = Tabulation $ \i ->
191+ (\(mk, a) -> (mk, (fromMaybe i mk, a))) <$> query i
354192
355193
356−-- | 'order' orders the /values/ of a 'Tabulation' within their
357−-- respective keys. This specifies a defined order for 'distinct'.
358−-- It also defines the order of the lists produced by 'many' and
359−-- 'some'.
360−order :: OrdTable k => Order a -> Tabulation k a -> Tabulation k a
361−order ordering (Tabulation f) =
362− Tabulation $ mapOpaleye (Opaleye.orderBy ordering') . f
363− where
364− Order ordering' = runClown (keyed (Clown ascTable) (Clown ordering))
194+ifilter :: (k -> a -> Expr Bool) -> Tabulation k a -> Tabulation k a
195+ifilter f tabulation = snd <$> do
196+ filter (uncurry f) `postbind` indexed tabulation
365197
366198
367−-- $magic
368−--
369−-- Some of the following combinators produce \"magic\" 'Tabulation's. Let's
370−-- use 'count' as an example to demonstrate this concept. Consider
371−-- the following:
372−--
373−-- @
374−-- count $ fromQuery $ values
375−-- [ (lit 'a', lit True)
376−-- , (lit 'a', lit False)
377−-- , (lit 'b', lit True)
378−-- ]
379−-- @
380−--
381−-- You might expect this to be equivalent to the following 'Tabulation':
382−--
383−-- @
384−-- fromQuery $ values
385−-- [ (lit 'a', 2)
386−-- , (lit 'b', 1)
387−-- ]
388−-- @
389−--
390−-- However, it isn't quite. While the resulting 'Tabulation' does effectively
391−-- contain the above entries, it also behaves as though it contained the value
392−-- @0@ at every other possible key.
393−--
394−-- This means you can do:
395−--
396−-- @
397−-- do
398−-- user <- usersById
399−-- orderCount <- count ordersByUserId
400−-- @
401−--
402−-- To see how many orders a user has (getting @0@ if they have no orders).
199+-- | Map a 'Query' over the input side of a 'Tabulation'.
200+prebind :: (a -> Tabulation k b) -> Query a -> Tabulation k b
201+prebind f as = Tabulation $ \k -> do
202+ a <- as
203+ case f a of
204+ Tabulation query -> query k
205+infixr 1 `prebind`
403206
404207
405−-- | 'count' returns a count of how many entries are in the given
406−-- 'Tabulation' at each key.
407−--
408−-- The resulting 'Tabulation' is \"magic\" in that the value @0@ exists at
409−-- every possible key that wasn't in the given 'Tabulation'.
410−count :: EqTable k => Tabulation k a -> Tabulation k (Expr Int64)
411−count =
412− fmap (maybeTable 0 id) .
413− optional .
414− aggregate .
415− fmap (const countStar)
416−
417−
418−-- | 'optional' produces a \"magic\" 'Tabulation' whereby each
419−-- entry in the given 'Tabulation' is wrapped in 'Rel8.justTable', and every
420−-- other possible key contains a single 'Rel8.nothingTable'.
421−--
422−-- This is used to implement 'leftAlignWith'.
423−optional :: Tabulation k a -> Tabulation k (MaybeTable Expr a)
424−optional (Tabulation f) = Tabulation $ \p -> case p of
425− Predicate Nothing -> fmap pure <$> f p
426− _ -> fmap (\m -> (empty, snd <$> m)) $ Q.optional $ do
427− (k, a) <- f p
428− ensure p k
429− pure (k, a)
430−
431−
432−-- | 'many' aggregates each entry with a particular key into a
433−-- single entry with all of the values contained in a 'ListTable'.
434−--
435−-- 'order' can be used to give this 'ListTable' a defined order.
436−--
437−-- The resulting 'Tabulation' is \"magic\" in that the value
438−-- @'Rel8.listTable []'@ exists at every possible key that wasn't in the given
439−-- 'Tabulation'.
440−many :: (EqTable k, Table Expr a)
441− => Tabulation k a -> Tabulation k (ListTable Expr a)
442−many =
443− fmap (maybeTable mempty (\(ListTable a) -> ListTable a)) .
444− optional .
445− aggregate .
446− fmap (listAgg . toCols)
447−
448−
449−-- | 'some' aggregates each entry with a particular key into a
450−-- single entry with all of the values contained in a 'NonEmptyTable'.
451−--
452−-- 'order' can be used to give this 'NonEmptyTable' a defined order.
453−some :: (EqTable k, Table Expr a)
454− => Tabulation k a -> Tabulation k (NonEmptyTable Expr a)
455−some =
456− fmap (\(NonEmptyTable a) -> NonEmptyTable a) .
457− aggregate .
458− fmap (nonEmptyAgg . toCols)
459−
460−
461−-- | 'exists' produces a \"magic\" 'Tabulation' which contains the
462−-- value 'Rel8.true' at each key in the given 'Tabulation', and the value
463−-- 'Rel8.false' at every other possible key.
464−exists :: Tabulation k a -> Tabulation k (Expr Bool)
465−exists (Tabulation f) = Tabulation $ \p -> case p of
466− Predicate Nothing -> (true <$) <$> f p
467− _ -> fmap (empty,) $ Q.exists $ do
468− (k, _) <- f p
469− ensure p k
470−
471−
472−-- | 'present' produces a 'Tabulation' where a single @()@ row
473−-- exists for every key that was present in the given 'Tabulation'.
474−--
475−-- This is used to implement 'similarity'.
476−present :: Tabulation k a -> Tabulation k ()
477−present (Tabulation f) = Tabulation $ \p -> do
478− Q.present $ do
479− (k, _) <- f p
480− ensure p k
481− pure (empty, ())
208+-- | Map a 'Query' over the output side of a 'Tabulation'.
209+postbind :: (a -> Query b) -> Tabulation k a -> Tabulation k b
210+postbind f (Tabulation as) = Tabulation $ \i -> do
211+ (k, a) <- as i
212+ case f a of
213+ bs -> do
214+ b <- bs
215+ pure (k, b)
216+infixr 1 `postbind`
482217
483218
484−-- | 'absent' produces a 'Tabulation' where a single @()@ row exists
485−-- at every possible key that absent from the given 'Tabulation'.
486−--
487−-- This is used to implement 'difference'.
488−absent :: Tabulation k a -> Tabulation k ()
489−absent (Tabulation f) = Tabulation $ \p -> do
490− Q.absent $ do
491− (k, _) <- f p
492− ensure p k
493− pure (empty, ())
219+-- | Note that because 'Tabulation' is a @MultiMap@, the 'Query' returned by
220+-- 'lookup' can and often does contain multiple results.
221+lookup :: EqTable k => k -> Tabulation k a -> Query a
222+lookup key (Tabulation query) = do
223+ (mk, a) <- query key
224+ traverse_ (where_ . (key ==:)) mk
225+ pure a
494226
495227
496−-- | Performs a @NATURAL FULL OUTER JOIN@ based on the common key columns.
228+-- | Analagous to
229+-- [@align@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:align).
497230--
498−-- Analogous to 'Data.Semialign.align'.
499−align :: EqTable k
500− => Tabulation k a -> Tabulation k b -> Tabulation k (TheseTable Expr a b)
231+-- If 'zip' makes an @INNER JOIN@, then 'align' makes a @FULL OUTER JOIN@.
232+align :: (EqTable k, Table Expr a, Table Expr b)
233+ => Tabulation k a -> Tabulation k b -> Tabulation k (TheseTable a b)
501234align = alignWith id
502235
503236
504−-- | Performs a @NATURAL FULL OUTER JOIN@ based on the common key columns.
237+-- | Analagous to
238+-- [@alignWith@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:alignWith).
505239--
506−-- Analogous to 'Data.Semialign.alignWith'.
507−alignWith :: EqTable k
508− => (TheseTable Expr a b -> c)
240+-- See 'zipWith' and 'align'.
241+alignWith :: (EqTable k, Table Expr a, Table Expr b)
242+ => (TheseTable a b -> c)
509243 -> Tabulation k a -> Tabulation k b -> Tabulation k c
510−alignWith f (Tabulation as) (Tabulation bs) = Tabulation $ \p -> do
511− tkab <- liftF2 (alignBy condition) as bs p
512− k <- traverse (rebind "key") $ recover $ bimap fst fst tkab
513− let
514− tab = bimap snd snd tkab
515− pure (k, f tab)
516− where
517− condition (k, _) (k', _) = fromMaybe true (liftA2 (==:) k k')
518− recover (TheseTable mma@(MaybeTable _ ma) mmb@(MaybeTable _ mb)) =
519− case extract ma of
520− Nothing -> extract mb
521− Just a -> case extract mb of
522− Nothing -> extract ma
523− Just b -> case a <$ mma <|>: b <$ mmb of
524− MaybeTable _ c -> pure (extract c)
525−
526−
527−-- | Performs a @NATURAL LEFT OUTER JOIN@ based on the common key columns.
528−--
529−-- Analogous to 'Data.Semialign.rpadZip'.
530−--
531−-- Note that you can achieve the same effect with 'optional' and the
532−-- 'Applicative' instance for 'Tabulation', i.e., this is just
533−-- @\left right -> liftA2 (,) left (optional right). You can also
534−-- use @do@-notation.
244+alignWith f kas kbs = do
245+ as <- toQuery kas
246+ bs <- toQuery kbs
247+ fromQuery $ do
248+ tkab <- alignBy (\(l, _) (m, _) -> l ==: m) as bs
249+ let
250+ k' = theseTable fst fst (const fst) tkab
251+ tab = bimap snd snd tkab
252+ pure (k', f tab)
253+
254+
255+-- | If 'zip' makes an @INNER JOIN@, then 'leftAlign' makes a @LEFT JOIN@.
256+-- This means it will return at least one row for every row in the left
257+-- 'Tabulation', even if there is no corresponding row in the right (hence the
258+-- 'Rel8.MaybeTable').
259+--
260+-- Analagous to
261+-- [@rpadZip@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:rpadZip).
535262leftAlign :: EqTable k
536− => Tabulation k a -> Tabulation k b -> Tabulation k (a, MaybeTable Expr b)
263+ => Tabulation k a -> Tabulation k b -> Tabulation k (a, MaybeTable b)
537264leftAlign = leftAlignWith (,)
538265
539266
540−-- | Performs a @NATURAL LEFT OUTER JOIN@ based on the common key columns.
267+-- | See 'zipWith' and 'leftAlign'.
541268--
542−-- Analogous to 'Data.Semialign.rpadZipWith'.
543−--
544−-- Note that you can achieve the same effect with 'optional' and the
545−-- 'Applicative' instance for 'Tabulation', i.e., this is just
546−-- @\f left right -> liftA2 f left (optional right). You can also
547−-- use @do@-notation.
269+-- Analagous to
270+-- [@rpadZipWith@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:rpadZipWith).
548271leftAlignWith :: EqTable k
549− => (a -> MaybeTable Expr b -> c)
272+ => (a -> MaybeTable b -> c)
550273 -> Tabulation k a -> Tabulation k b -> Tabulation k c
551−leftAlignWith f left right = liftA2 f left (optional right)
274+leftAlignWith f left right = liftA2 f left (optionalTabulation right)
552275
553276
554−-- | Performs a @NATURAL RIGHT OUTER JOIN@ based on the common key columns.
555−--
556−-- Analogous to 'Data.Semialign.lpadZip'.
277+-- | Analagous to
278+-- [@zip@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:zip).
557279--
558−-- Note that you can achieve the same effect with 'optional' and the
559−-- 'Applicative' instance for 'Tabulation', i.e., this is just
560−-- @\left right -> liftA2 (flip (,)) right (optional left). You can
561−-- also use @do@-notation.
562−rightAlign :: EqTable k
563− => Tabulation k a -> Tabulation k b -> Tabulation k (MaybeTable Expr a, b)
564−rightAlign = rightAlignWith (,)
565−
566−
567−-- | Performs a @NATURAL RIGHT OUTER JOIN@ based on the common key columns.
280+-- There are multiple correct ways of understanding what this does.
568281--
569−-- Analogous to 'Data.Semialign.lpadZipWith'.
282+-- You can think of it as @'Data.Map.Strict.intersectionWith'
283+-- ('Control.Applicative.liftA2' (,))@. That is, @intersect@ the two
284+-- `Tabulation`s by matching their keys together (with 'Rel8.==:'), and combine
285+-- their values (remembering that 'Tabulation' is a 'MultiMap' so that the
286+-- values are keys) by getting their cartesian product.
570287--
571−-- Note that you can achieve the same effect with 'optional' and the
572−-- 'Applicative' instance for 'Tabulation', i.e., this is just
573−-- @\f left right -> liftA2 (flip f) right (optional left). You can
574−-- also use @do@-notation.
575−rightAlignWith :: EqTable k
576− => (MaybeTable Expr a -> b -> c)
577− -> Tabulation k a -> Tabulation k b -> Tabulation k c
578−rightAlignWith f left right = liftA2 (flip f) right (optional left)
579−
580−
581−-- | Performs a @NATURAL INNER JOIN@ based on the common key columns.
288+-- You can think of it as performing a cross product of the underlying 'Query's
289+-- of the given 'Tabulation's and filtering the results for 'match'ing keys.
582290--
583−-- Analagous to 'Data.Semialign.zip'.
291+-- You can think of it as a natural join in SQL terms.
584292--
585−-- Note that you can achieve the same effect with the 'Applicative' instance
586−-- of 'Tabulation', i.e., this is just @'liftA2 (,)'@. You can also use
587−-- @do@-notation.
293+-- The size of the resulting 'Tabulation' will be \(\sum_{k} min(n_k, m_k) \)
294+-- in terms of the number of keys, but \(\sum_{k} n_k \times m_k\) in terms of
295+-- the number of values.
588296zip :: EqTable k
589297 => Tabulation k a -> Tabulation k b -> Tabulation k (a, b)
590298zip = zipWith (,)
591299
592300
593−-- | Performs a @NATURAL INNER JOIN@ based on the common key columns.
301+-- | Analagous to
302+-- [@zipWith@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:zipWith).
594303--
595−-- Analagous to 'Data.Semialign.zipWith'.
596−--
597−-- Note that you can achieve the same effect with the 'Applicative' instance
598−-- of 'Tabulation', i.e., this is just @'liftA2'@. You can also use
599−-- @do@-notation.
304+-- See 'zip'.
600305zipWith :: EqTable k
601306 => (a -> b -> c) -> Tabulation k a -> Tabulation k b -> Tabulation k c
602307zipWith = liftA2
603308
604309
605−-- | Performs a [@NATURAL SEMI JOIN@](https://en.wikipedia.org/wiki/Relational_algebra#Semijoin_%28%E2%8B%89%29%28%E2%8B%8A%29)
606−-- based on the common key columns.
310+-- | 'similarity' returns all the entries in the left 'Tabulation' that have a
311+-- corresponding entry in the right 'Tabulation'. This corresponds to a
312+-- semijoin in relational algebra.
607313--
608−-- The result is a subset of the left tabulation where only entries which have
609−-- a corresponding entry in the right tabulation are kept.
314+-- This differs from @'zipWith' const x y@ when the right 'Tabulation' @y@
315+-- contains an entry with multiple rows. For 'similarity', the entries in the
316+-- resulting 'Tabulation' will contain the same number of rows as their
317+-- respective entries in the left 'Tabulation' @x@. With `zipWith const x y`,
318+-- each entry would contain the /product/ of the number of rows of their
319+-- respective entries in @x@ and @y@.
610320--
611−-- Note that you can achieve a similar effect with 'present' and the
612−-- 'Applicative' instance of 'Tabulation', i.e., this is just
613−-- @\left right -> left <* present right@. You can also use
614−-- @do@-notation.
321+-- See 'Rel8.with'.
615322similarity :: EqTable k => Tabulation k a -> Tabulation k b -> Tabulation k a
616−similarity a b = a <* present b
323+similarity kas kbs = do
324+ as <- toQuery kas
325+ bs <- toQuery kbs
326+ fromQuery $ as >>= withBy (\(k, _) (l, _) -> k ==: l) bs
617327
618328
619−-- | Performs a [@NATURAL ANTI JOIN@](https://en.wikipedia.org/wiki/Relational_algebra#Antijoin_%28%E2%96%B7%29)
620−-- based on the common key columns.
621−--
622−-- The result is a subset of the left tabulation where only entries which do
623−-- not have a corresponding entry in the right tabulation are kept.
329+-- | 'difference' returns all the entries in the left 'Tabulation' that don't
330+-- exist in the right 'Tabulation'. This corresponds to an antijoin in
331+-- relational algebra.
624332--
625−-- Note that you can achieve a similar effect with 'absent' and the
626−-- 'Applicative' instance of 'Tabulation', i.e., this is just
627−-- @\left right -> left <* absent right@. You can also use
628−-- @do@-notation.
333+-- See 'Rel8.without'.
629334difference :: EqTable k => Tabulation k a -> Tabulation k b -> Tabulation k a
630−difference a b = a <* absent b
335+difference kas kbs = do
336+ as <- toQuery kas
337+ bs <- toQuery kbs
338+ fromQuery $ as >>= withoutBy (\(k, _) (l, _) -> k ==: l) bs
339+
340+
341+aggregateTabulation :: EqTable k
342+ => Tabulation k (Aggregate a) -> Tabulation k a
343+aggregateTabulation = mapQuery (aggregate . fmap (bitraverse1 groupBy id))
344+
345+
346+-- | 'orderTabulation' orders the /values/ of a 'Tabulation' within each key.
347+--
348+-- In general this is meaningless, but if used together with 'manyTabulation'
349+-- or 'someTabulation', the resulting lists will be ordered according to
350+-- ordering given to 'orderTabulation'.
351+orderTabulation :: OrdTable k => Order a -> Tabulation k a -> Tabulation k a
352+orderTabulation ordering = mapQuery $ orderBy ordering'
353+ where
354+ ordering' = (fst >$< ascTable) <> (snd >$< ordering)
355+
356+
357+-- | Turns the given 'Tabulation' from a \"multimap\" into a \"map\". If there
358+-- is more than one value at a particular key, only the first one is kept.
359+-- \"First\" is in general undefined, but 'orderTabulation' can be used to
360+-- make it deterministic.
361+distinctTabulation :: EqTable k => Tabulation k a -> Tabulation k a
362+distinctTabulation = mapQuery (distinctOn fst)
363+
364+
365+optionalTabulation :: EqTable k
366+ => Tabulation k a -> Tabulation k (MaybeTable a)
367+optionalTabulation as = Tabulation $ \k -> do
368+ ma <- optional $ lookup k as
369+ pure (Just k, ma)
370+
371+
372+manyTabulation :: (EqTable k, Table Expr a)
373+ => Tabulation k a -> Tabulation k (ListTable a)
374+manyTabulation =
375+ fmap (maybeTable mempty id) .
376+ optionalTabulation .
377+ aggregateTabulation .
378+ fmap listAgg
379+
380+
381+someTabulation :: (EqTable k, Table Expr a)
382+ => Tabulation k a -> Tabulation k (NonEmptyTable a)
383+someTabulation = aggregateTabulation . fmap nonEmptyAgg
384+
385+
386+mapQuery :: (Query (k, a) -> Query (k, b)) -> Tabulation k a -> Tabulation k b
387+mapQuery f (Tabulation query) = Tabulation $ \k ->
388+ first Just <$> f (first (fromMaybe k) <$> query k)
631389
632390
633−-- | 'Tabulation's can be produced with either 'fromQuery' or 'liftQuery', and
634−-- in some cases we might want to treat these differently. 'peek' uses
635−-- 'unsafePeekQuery' to determine which type of 'Tabulation' we have.
636−peek :: Tabulation k a -> Tabulation k (Either (Query a) (Query (k, a)))
637−peek (Tabulation f) = Tabulation $ \p ->
638− pure $ (empty,) $ case unsafePeekQuery (f p) of
639− (Nothing, _) -> Left $ fmap snd (f p)
640− (Just _, _) -> Right $ fmap (first fromJust) (f p)
391+toQuery :: Tabulation k a -> Tabulation k (Query (k, a))
392+toQuery (Tabulation as) = Tabulation $ \k ->
393+ pure (Nothing, first (fromMaybe k) <$> as k)