@@ -1,254 +1,130 @@
1+{-# language DeriveFunctor #-}
2+{-# language DerivingStrategies #-}
13{-# language FlexibleContexts #-}
2−{-# language MonoLocalBinds #-}
3−{-# language ScopedTypeVariables #-}
4−{-# language StandaloneKindSignatures #-}
5−{-# language TypeApplications #-}
4+{-# language LambdaCase #-}
65{-# language TupleSections #-}
76{-# language UndecidableInstances #-}
87
9−-- | "Rel8.Tabulate" provides an alternative API ('Tabulation') for writing
10−-- queries that complements the main "Rel8" API ('Query').
11−
128module Rel8.Tabulate
13− (
14− Tabulation
15−
16− -- * Interfacing with 'Query's
9+ ( Tabulation
10+ , runTabulation
11+ , tabulate
12+ , tabulateA
1713 , fromQuery
18− , toQuery
1914 , liftQuery
20− , through
15+ , prebind
16+ , postbind
17+ , indexed
18+ , ifilter
2119 , 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
3920 , align
4021 , alignWith
4122 , leftAlign
4223 , leftAlignWith
43− , rightAlign
44− , rightAlignWith
4524 , zip
4625 , zipWith
4726 , similarity
4827 , difference
28+ , aggregateTabulation
29+ , orderTabulation
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, undefined, 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.Expr.Bool ( not_, true )
49+import Rel8.Order ( Order )
9050import 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 )
51+import Rel8.Query.Aggregate ( aggregate )
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 )
110−import Rel8.Table.Ord ( OrdTable )
111−import 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 )
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 )
67+import Rel8.Table.These ( TheseTable, theseTable )
68+import Rel8.Table.Undefined ( undefined )
11869
11970-- semigroupoids
12071import 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
72+import Data.Functor.Bind ( Bind )
73+import qualified Data.Functor.Bind
74+import Data.Semigroup.Traversable.Class ( bitraverse1 )
14375
14476
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−
164−
165−ensure :: Predicate k -> Key k -> Query ()
166−ensure (Predicate mp) = traverse_ (\k -> traverse_ (\p -> where_ (p k)) mp)
167−
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.
77+-- | @'Tabulation' k a@ is denotionally a @MultiMap k a@ — a @Map@ where each
78+-- key @k@ corresponds to potentially multiple @a@ (i.e., @'Query' a@). This
79+-- @MultiMap@ supports 'lookup' and other operations you would expect it to.
17980--
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))
81+-- \"Identity\" 'Tabulation's are created using 'tabulate'. 'Tabulation's can
82+-- be composed with 'Query's with 'prebind' or 'postbind' to form new
83+-- 'Tabulation's.
84+newtype Tabulation k a = Tabulation (Either k k -> Query (Maybe k, a))
85+ deriving stock Functor
18586
18687
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−
198−
199−instance Functor (Tabulation k) where
200− fmap = second
201−
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 (<*>))@
20988instance EqTable k => Apply (Tabulation k) where
21089 liftF2 = liftA2
21190
21291
213−-- | @pure = 'liftQuery' . pure@
21492instance EqTable k => Applicative (Tabulation k) where
21593 pure = liftQuery . pure
21694 liftA2 = liftM2
21795
21896
21997instance 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)
98+ join = join
22899
229100
230101instance EqTable k => Monad (Tabulation k) where
231− (>>=) = (>>-)
102+ Tabulation as >>= f = Tabulation $ \i -> do
103+ (mk, a) <- as i
104+ case mk of
105+ Nothing -> case f a of
106+ Tabulation bs -> bs i
107+ Just k -> case f a of
108+ Tabulation bs -> do
109+ (mk', b) <- bs (Right k)
110+ case mk' of
111+ Nothing -> pure (mk, b)
112+ Just k' -> do
113+ where_ $ k ==: k'
114+ pure (mk', b)
232115
233116
234−-- | If @'Tabulation' k a@ is @Map k (NonEmpty a)@, then @(<|>:)@ is
235−-- @unionWith (<>)@.
236117instance 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
118+ kas <|>: kbs = do
119+ as <- toQuery kas
120+ bs <- toQuery kbs
121+ fromQuery $ as <|>: bs
244122
245123
246124instance EqTable k => AlternativeTable (Tabulation k) where
247− emptyTable = Tabulation $ const $ fmap (empty,) emptyTable
125+ emptyTable = fromQuery emptyTable
248126
249127
250−-- | If @'Tabulation' k a@ is @Map k (NonEmpty a)@, then @(<>)@ is
251−-- @unionWith (liftA2 (<>))@.
252128instance (EqTable k, Table Expr a, Semigroup a) => Semigroup (Tabulation k a)
253129 where
254130 (<>) = alignWith (theseTable id id (<>))
@@ -259,382 +135,263 @@ instance (EqTable k, Table Expr a, Semigroup a) => Monoid (Tabulation k a)
259135 mempty = emptyTable
260136
261137
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)
265−
266−
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.
272−--
273−-- A safer, more predictable alternative to 'toQuery' is to use 'lookup' with
274−-- an explicit set of keys:
138+-- | This "undoes" 'fromQuery'.
275139--
276140-- @
277−-- do
278−-- k <- keys
279−-- a <- lookup k tabulation
280−-- pure (k, a)
141+-- 'runTabulation' . 'fromQuery' = id
142+-- 'fromQuery' . 'runTabulation' = id
281143-- @
282144--
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.
288−--
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,)
306−
307−
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`
145+-- Note however that it produces nonsense when passed a 'Tabulation' that was
146+-- made with from 'liftQuery' or 'pure'. In such cases the returned 'Query'
147+-- will always produce zero rows.
148+runTabulation :: EqTable k => Tabulation k a -> Query (k, a)
149+runTabulation (Tabulation f) = do
150+ (mk, a) <- f (Left undefined)
151+ case mk of
152+ Nothing -> do
153+ -- Opaleye tries to be too clever optimising "WHERE FALSE" and generates
154+ -- invalid SQL, so we use "WHERE NOT TRUE" instead
155+ where_ $ not_ true
156+ pure (undefined, a)
157+ Just k -> pure (k, a)
320158
321159
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)
160+liftQuery :: Query a -> Tabulation k a
161+liftQuery query = Tabulation $ const $ fmap (Nothing,) query
330162
331163
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
345−
346−
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
354−
355−
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))
365−
366−
367−-- $magic
164+-- | 'tabulate' creates an \"identity\" @'Tabulation' k a@ that allows @a@ be
165+-- indexed by one or more of its columns @k@. Some examples:
368166--
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:
167+-- [Tabulation by primary key]:
168+-- @
169+-- projectsById :: Project 'Expr' -> 'Tabulation' ('Expr' ProjectId) (Project 'Expr')
170+-- projectsById = 'tabulate' projectId
171+-- @
372172--
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':
173+-- Note: the nature of primary keys means that each key will be mapped to a
174+-- singleton value in this case.
382175--
383−-- @
384−-- fromQuery $ values
385−-- [ (lit 'a', 2)
386−-- , (lit 'b', 1)
387−-- ]
388−-- @
176+-- [Tabulation by other unique key]:
177+-- @
178+-- projectsByName :: Project 'Expr' -> 'Tabulation' ('Expr' Text) (Project 'Expr')
179+-- projectsByName = 'tabulate' projectName
180+-- @
389181--
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).
182+-- [Tabulation by foreign key (tabulate a child table by parent key)]:
183+-- @
184+-- revisionsByProjectId :: Revision 'Expr' -> 'Tabulation' ('Expr' ProjectId) (Revision 'Expr')
185+-- revisionsByProjectId = 'tabulate' revisionProjectId
186+-- @
187+tabulate :: (a -> k) -> a -> Tabulation k a
188+tabulate key a = fromQuery $ pure (key a, a)
403189
404190
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, ())
191+-- | Like 'tabulate' but takes a monadic 'Query' function instead of a pure
192+-- one. This means you can filter rows while calculating the key, which is
193+-- useful in conjunction with 'Rel8.Extra.catNulls'.
194+tabulateA :: (a -> Query k) -> a -> Tabulation k a
195+tabulateA key a = fromQuery $ (,a) <$> key a
482196
483197
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, ())
198+-- | Analgous to 'Data.Map.Strict.fromList'.
199+fromQuery :: Query (k, a) -> Tabulation k a
200+fromQuery = Tabulation . const . fmap (first Just)
201+
202+
203+indexed :: Tabulation k a -> Tabulation k (k, a)
204+indexed (Tabulation query) = Tabulation $ \i ->
205+ (\(mk, a) -> (mk, (fromMaybe (either id id i) mk, a))) <$> query i
206+
207+
208+ifilter :: (k -> a -> Expr Bool) -> Tabulation k a -> Tabulation k a
209+ifilter f tabulation = snd <$> do
210+ filter (uncurry f) `postbind` indexed tabulation
211+
212+
213+-- | Map a 'Query' over the input side of a 'Tabulation'.
214+prebind :: (a -> Tabulation k b) -> Query a -> Tabulation k b
215+prebind f as = Tabulation $ \k -> do
216+ a <- as
217+ case f a of
218+ Tabulation query -> query k
219+infixr 1 `prebind`
220+
221+
222+-- | Map a 'Query' over the output side of a 'Tabulation'.
223+postbind :: (a -> Query b) -> Tabulation k a -> Tabulation k b
224+postbind f (Tabulation as) = Tabulation $ \i -> do
225+ (k, a) <- as i
226+ case f a of
227+ bs -> do
228+ b <- bs
229+ pure (k, b)
230+infixr 1 `postbind`
494231
495232
496−-- | Performs a @NATURAL FULL OUTER JOIN@ based on the common key columns.
233+-- | Note that because 'Tabulation' is a @MultiMap@, the 'Query' returned by
234+-- 'lookup' can and often does contain multiple results.
235+lookup :: EqTable k => k -> Tabulation k a -> Query a
236+lookup key (Tabulation query) = do
237+ (mk, a) <- query (Right key)
238+ traverse_ (where_ . (key ==:)) mk
239+ pure a
240+
241+
242+-- | Analagous to
243+-- [@align@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:align).
497244--
498−-- Analogous to 'Data.Semialign.align'.
499−align :: EqTable k
500− => Tabulation k a -> Tabulation k b -> Tabulation k (TheseTable Expr a b)
245+-- If 'zip' makes an @INNER JOIN@, then 'align' makes a @FULL OUTER JOIN@.
246+align :: (EqTable k, Table Expr a, Table Expr b)
247+ => Tabulation k a -> Tabulation k b -> Tabulation k (TheseTable a b)
501248align = alignWith id
502249
503250
504−-- | Performs a @NATURAL FULL OUTER JOIN@ based on the common key columns.
251+-- | Analagous to
252+-- [@alignWith@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:alignWith).
505253--
506−-- Analogous to 'Data.Semialign.alignWith'.
507−alignWith :: EqTable k
508− => (TheseTable Expr a b -> c)
254+-- See 'zipWith' and 'align'.
255+alignWith :: (EqTable k, Table Expr a, Table Expr b)
256+ => (TheseTable a b -> c)
509257 -> 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.
258+alignWith f kas kbs = do
259+ as <- toQuery kas
260+ bs <- toQuery kbs
261+ fromQuery $ do
262+ tkab <- alignBy (\(l, _) (m, _) -> l ==: m) as bs
263+ let
264+ k' = theseTable fst fst (const fst) tkab
265+ tab = bimap snd snd tkab
266+ pure (k', f tab)
267+
268+
269+-- | If 'zip' makes an @INNER JOIN@, then 'leftAlign' makes a @LEFT JOIN@.
270+-- This means it will return at least one row for every row in the left
271+-- 'Tabulation', even if there is no corresponding row in the right (hence the
272+-- 'Rel8.MaybeTable').
273+--
274+-- Analagous to
275+-- [@rpadZip@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:rpadZip).
535276leftAlign :: EqTable k
536− => Tabulation k a -> Tabulation k b -> Tabulation k (a, MaybeTable Expr b)
277+ => Tabulation k a -> Tabulation k b -> Tabulation k (a, MaybeTable b)
537278leftAlign = leftAlignWith (,)
538279
539280
540−-- | Performs a @NATURAL LEFT OUTER JOIN@ based on the common key columns.
541−--
542−-- Analogous to 'Data.Semialign.rpadZipWith'.
281+-- | See 'zipWith' and 'leftAlign'.
543282--
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.
283+-- Analagous to
284+-- [@rpadZipWith@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:rpadZipWith).
548285leftAlignWith :: EqTable k
549− => (a -> MaybeTable Expr b -> c)
286+ => (a -> MaybeTable b -> c)
550287 -> Tabulation k a -> Tabulation k b -> Tabulation k c
551−leftAlignWith f left right = liftA2 f left (optional right)
288+leftAlignWith f left right = liftA2 f left (optionalTabulation right)
552289
553290
554−-- | Performs a @NATURAL RIGHT OUTER JOIN@ based on the common key columns.
291+-- | Analagous to
292+-- [@zip@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:zip).
555293--
556−-- Analogous to 'Data.Semialign.lpadZip'.
294+-- There are multiple correct ways of understanding what this does.
557295--
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.
568−--
569−-- Analogous to 'Data.Semialign.lpadZipWith'.
296+-- You can think of it as @'Data.Map.Strict.intersectionWith'
297+-- ('Control.Applicative.liftA2' (,))@. That is, @intersect@ the two
298+-- `Tabulation`s by matching their keys together (with 'Rel8.==:'), and combine
299+-- their values (remembering that 'Tabulation' is a 'MultiMap' so that the
300+-- values are keys) by getting their cartesian product.
570301--
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.
302+-- You can think of it as performing a cross product of the underlying 'Query's
303+-- of the given 'Tabulation's and filtering the results for 'match'ing keys.
582304--
583−-- Analagous to 'Data.Semialign.zip'.
305+-- You can think of it as a natural join in SQL terms.
584306--
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.
307+-- The size of the resulting 'Tabulation' will be \(\sum_{k} min(n_k, m_k) \)
308+-- in terms of the number of keys, but \(\sum_{k} n_k \times m_k\) in terms of
309+-- the number of values.
588310zip :: EqTable k
589311 => Tabulation k a -> Tabulation k b -> Tabulation k (a, b)
590312zip = zipWith (,)
591313
592314
593−-- | Performs a @NATURAL INNER JOIN@ based on the common key columns.
315+-- | Analagous to
316+-- [@zipWith@](https://hackage.haskell.org/package/semialign/docs/Data-Semialign.html#v:zipWith).
594317--
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.
318+-- See 'zip'.
600319zipWith :: EqTable k
601320 => (a -> b -> c) -> Tabulation k a -> Tabulation k b -> Tabulation k c
602321zipWith = liftA2
603322
604323
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.
324+-- | 'similarity' returns all the entries in the left 'Tabulation' that have a
325+-- corresponding entry in the right 'Tabulation'. This corresponds to a
326+-- semijoin in relational algebra.
607327--
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.
328+-- This differs from @'zipWith' const x y@ when the right 'Tabulation' @y@
329+-- contains an entry with multiple rows. For 'similarity', the entries in the
330+-- resulting 'Tabulation' will contain the same number of rows as their
331+-- respective entries in the left 'Tabulation' @x@. With `zipWith const x y`,
332+-- each entry would contain the /product/ of the number of rows of their
333+-- respective entries in @x@ and @y@.
610334--
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.
335+-- See 'Rel8.with'.
615336similarity :: EqTable k => Tabulation k a -> Tabulation k b -> Tabulation k a
616−similarity a b = a <* present b
337+similarity kas kbs = do
338+ as <- toQuery kas
339+ bs <- toQuery kbs
340+ fromQuery $ as >>= withBy (\(k, _) (l, _) -> k ==: l) bs
617341
618342
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.
343+-- | 'difference' returns all the entries in the left 'Tabulation' that don't
344+-- exist in the right 'Tabulation'. This corresponds to an antijoin in
345+-- relational algebra.
624346--
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.
347+-- See 'Rel8.without'.
629348difference :: EqTable k => Tabulation k a -> Tabulation k b -> Tabulation k a
630−difference a b = a <* absent b
349+difference kas kbs = do
350+ as <- toQuery kas
351+ bs <- toQuery kbs
352+ fromQuery $ as >>= withoutBy (\(k, _) (l, _) -> k ==: l) bs
353+
354+
355+aggregateTabulation :: EqTable k
356+ => Tabulation k (Aggregate a) -> Tabulation k a
357+aggregateTabulation kas = do
358+ as <- toQuery kas
359+ fromQuery $ aggregate $ bitraverse1 groupBy id <$> as
360+
361+
362+-- | 'orderTabulation' orders the /values/ of a 'Tabulation' (not the keys).
363+-- In general this is meaningless, but if used together with 'manyTabulation'
364+-- or 'someTabulation', the resulting lists will be ordered according to
365+-- ordering given to 'orderTabulation'.
366+orderTabulation :: Order a -> Tabulation k a -> Tabulation k a
367+orderTabulation ordering (Tabulation as) =
368+ Tabulation $ orderBy (snd >$< ordering) . as
369+
370+
371+optionalTabulation :: EqTable k
372+ => Tabulation k a -> Tabulation k (MaybeTable a)
373+optionalTabulation as = Tabulation $ \case
374+ Left k -> case as of
375+ Tabulation f -> fmap pure <$> f (Left k)
376+ Right k -> do
377+ ma <- optional $ lookup k as
378+ pure (Just k, ma)
379+
380+
381+manyTabulation :: (EqTable k, Table Expr a)
382+ => Tabulation k a -> Tabulation k (ListTable a)
383+manyTabulation =
384+ fmap (maybeTable mempty id) .
385+ optionalTabulation .
386+ aggregateTabulation .
387+ fmap listAgg
388+
389+
390+someTabulation :: (EqTable k, Table Expr a)
391+ => Tabulation k a -> Tabulation k (NonEmptyTable a)
392+someTabulation = aggregateTabulation . fmap nonEmptyAgg
631393
632394
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)
395+toQuery :: Tabulation k a -> Tabulation k (Query (k, a))
396+toQuery (Tabulation as) = Tabulation $ \k ->
397+ pure (Nothing, first (fromMaybe (either id id k)) <$> as k)