rel8-1.7.0.0: src/Rel8/Aggregate.hs
{-# language DataKinds #-}
{-# language GADTs #-}
{-# language KindSignatures #-}
{-# language ScopedTypeVariables #-}
{-# language StandaloneKindSignatures #-}
module Rel8.Aggregate
( Aggregator' (Aggregator)
, Aggregator
, Aggregator1
, toAggregator
, toAggregator1
, filterWhereExplicit
, unsafeMakeAggregator
)
where
-- base
import Control.Applicative (liftA2)
import Data.Kind (Type)
import Prelude
-- opaleye
import qualified Opaleye.Aggregate as Opaleye
import qualified Opaleye.Internal.MaybeFields as Opaleye
import qualified Opaleye.Internal.Operators as Opaleye
-- product-profunctor
import Data.Profunctor.Product
( ProductProfunctor, purePP, (****)
, SumProfunctor, (+++!)
)
-- profunctors
import Data.Profunctor (Profunctor, dimap)
-- rel8
import Rel8.Expr (Expr)
import Rel8.Expr.Opaleye (toPrimExpr, toColumn)
import Rel8.Aggregate.Fold (Fallback (Empty, Fallback), Fold (Full, Semi))
-- semigroupoids
import Data.Functor.Apply (Apply, liftF2)
-- | 'Aggregator'' is the most general form of \"aggregator\", of which
-- 'Aggregator' and 'Aggregator1' are special cases. 'Aggregator''s are
-- comprised of aggregation functions and/or @GROUP BY@ clauses.
--
-- Aggregation functions operating on individual 'Rel8.Expr's such as
-- 'Rel8.sum' can be combined into 'Aggregator's operating on larger types
-- using the 'Applicative', 'Profunctor' and 'ProductProfunctor' interfaces.
-- Working with 'Profunctor's can sometimes be awkward so for every 'Rel8.sum'
-- we also provide a 'Rel8.sumOn' which bundles an 'Data.Profunctor.lmap'. For
-- complex aggregations, we recommend using these functions along with
-- @ApplicativeDo@, @BlockArguments@, @OverloadedRecordDot@ and
-- @RecordWildCards@:
--
-- @
--
-- data Input f = Input
-- { orderId :: Column f OrderId
-- , customerId :: Column f CustomerId
-- , productId :: Column f ProductId
-- , quantity :: Column f Int64
-- , price :: Column f Scientific
-- }
-- deriving (Generic, Rel8able)
--
--
-- totalPrice :: Input Expr -> Expr Scientific
-- totalPrice input = fromIntegral input.quantity * input.price
--
--
-- data Result f = Result
-- { customerId :: Column f CustomerId
-- , totalOrders :: Column f Int64
-- , productsOrdered :: Column f Int64
-- , totalPrice :: Column f Scientific
-- }
-- deriving (Generic, Rel8able)
--
--
-- allResults :: Query (Result Expr)
-- allResults =
-- aggregate
-- do
-- customerId <- groupByOn (.customerId)
-- totalOrders <- countDistinctOn (.orderId)
-- productsOrdered <- countDistinctOn (.productId)
-- totalPrice <- sumOn totalPrice
-- pure Result {..}
-- do
-- order <- each orderSchema
-- orderLine <- each orderLineSchema
-- where_ $ order.id ==. orderLine.orderId
-- pure
-- Input
-- { orderId = order.id
-- , customerId = order.customerId
-- , productId = orderLine.productId
-- , quantity = orderLine.quantity
-- , price = orderLine.price
-- }
-- @
type Aggregator' :: Fold -> Type -> Type -> Type
data Aggregator' fold i a = Aggregator !(Fallback fold a) !(Opaleye.Aggregator i a)
instance Profunctor (Aggregator' fold) where
dimap f g (Aggregator fallback a) =
Aggregator (fmap g fallback) (dimap f g a)
instance ProductProfunctor (Aggregator' fold) where
purePP = pure
(****) = (<*>)
instance SumProfunctor (Aggregator' fold) where
Aggregator fallback a +++! Aggregator fallback' b =
flip Aggregator (a +++! b) $ case fallback of
Empty -> case fallback' of
Empty -> Empty
Fallback x -> Fallback (Right x)
Fallback x -> Fallback (Left x)
instance Functor (Aggregator' fold i) where
fmap = dimap id
instance Apply (Aggregator' fold i) where
liftF2 f (Aggregator fallback a) (Aggregator fallback' b) =
Aggregator (liftF2 f fallback fallback') (liftA2 f a b)
instance Applicative (Aggregator' fold i) where
pure a = Aggregator (pure a) (pure a)
liftA2 = liftF2
-- | An 'Aggregator' takes a 'Rel8.Query' producing a collection of rows of
-- type @a@ and transforms it into a 'Rel8.Query' producing a single row of
-- type @b@. If the given 'Rel8.Query' produces an empty collection of rows,
-- then the single row in the resulting 'Rel8.Query' contains the identity
-- values of the aggregation functions comprising the 'Aggregator' (i.e.,
-- @0@ for 'Rel8.sum', 'Rel8.false' for 'Rel8.or', etc.).
--
-- 'Aggregator' is a special form of 'Aggregator'' parameterised by 'Full'.
type Aggregator :: Type -> Type -> Type
type Aggregator = Aggregator' 'Full
-- | An 'Aggregator1' takes a collection of rows of type @a@, groups them, and
-- transforms each group into a single row of type @b@. This corresponds to
-- aggregators using @GROUP BY@ in SQL. If given an empty collection of rows,
-- 'Aggregator1' will have no groups and will therefore also return an empty
-- collection of rows.
--
-- 'Aggregator1' is a special form of 'Aggregator'' parameterised by 'Semi'.
type Aggregator1 :: Type -> Type -> Type
type Aggregator1 = Aggregator' 'Semi
-- | 'toAggregator1' turns an 'Aggregator' into an 'Aggregator1'.
toAggregator1 :: Aggregator' fold i a -> Aggregator1 i a
toAggregator1 (Aggregator _ a) = Aggregator Empty a
-- | Given a value to fall back on if given an empty collection of rows,
-- 'toAggregator' turns an 'Aggregator1' into an 'Aggregator'.
toAggregator :: a -> Aggregator' fold i a -> Aggregator' fold' i a
toAggregator fallback (Aggregator _ a) = Aggregator (Fallback fallback) a
filterWhereExplicit :: ()
=> Opaleye.IfPP a a
-> (i -> Expr Bool)
-> Aggregator i a
-> Aggregator' fold i a
filterWhereExplicit ifPP f (Aggregator (Fallback fallback) aggregator) =
Aggregator (Fallback fallback) aggregator'
where
aggregator' =
Opaleye.fromMaybeFieldsExplicit ifPP fallback
<$> Opaleye.filterWhere (toColumn . toPrimExpr . f) aggregator
unsafeMakeAggregator :: forall (i :: Type) (o :: Type) (fold :: Fold) i' o'. ()
=> (i -> i')
-> (o' -> o)
-> Fallback fold o
-> Opaleye.Aggregator i' o'
-> Aggregator' fold i o
unsafeMakeAggregator input output fallback =
Aggregator fallback . dimap input output