rel8-1.7.0.0: src/Rel8/Table/Aggregate.hs
{-# language BlockArguments #-}
{-# language FlexibleContexts #-}
{-# language NamedFieldPuns #-}
{-# language ScopedTypeVariables #-}
{-# language TypeApplications #-}
{-# language TypeFamilies #-}
module Rel8.Table.Aggregate
( groupBy, groupByOn
, listAgg, listAggOn, nonEmptyAgg, nonEmptyAggOn
, listCat, listCatOn, nonEmptyCat, nonEmptyCatOn
, filterWhere
, orderAggregateBy
)
where
-- base
import Prelude
-- opaleye
import qualified Opaleye.Internal.Aggregate as Opaleye
-- profunctors
import Data.Profunctor (dimap, lmap)
-- rel8
import Rel8.Aggregate (Aggregator, Aggregator' (Aggregator), Aggregator1)
import Rel8.Expr ( Expr )
import Rel8.Expr.Aggregate
( filterWhereExplicit
, groupByExprOn
, slistAggExpr
, slistCatExpr
, snonEmptyAggExpr
, snonEmptyCatExpr
)
import Rel8.Order (Order (Order))
import Rel8.Schema.Dict ( Dict( Dict ) )
import Rel8.Schema.HTable (HTable, hfield, hspecs, htabulateA)
import Rel8.Schema.HTable.Vectorize (htraverseVectorP, hvectorizeA)
import Rel8.Schema.Null ( Sql )
import Rel8.Schema.Spec ( Spec( Spec, info ) )
import Rel8.Table (Table, toColumns, fromColumns)
import Rel8.Table.Eq ( EqTable, eqTable )
import Rel8.Table.List ( ListTable )
import Rel8.Table.NonEmpty ( NonEmptyTable )
import Rel8.Table.Opaleye (ifPP)
import Rel8.Type.Eq ( DBEq )
-- | Group equal tables together. This works by aggregating each column in the
-- given table with 'groupByExpr'.
--
-- For example, if we have a table of items, we could group the items by the
-- order they belong to:
--
-- @
-- itemsByOrder :: Query (OrderId Expr, ListTable Expr (Item Expr))
-- itemsByOrder =
-- aggregate
-- do
-- orderId <- groupByOn (.orderId)
-- items <- listAgg
-- pure (orderId, items)
-- do
-- each itemSchema
-- @
groupBy :: forall a. EqTable a => Aggregator1 a a
groupBy = dimap toColumns fromColumns (hgroupBy (eqTable @a))
-- | Applies 'groupBy' to the columns selected by the given function.
groupByOn :: EqTable a => (i -> a) -> Aggregator1 i a
groupByOn f = lmap f groupBy
hgroupBy :: HTable t => t (Dict (Sql DBEq)) -> Aggregator1 (t Expr) (t Expr)
hgroupBy eqs = htabulateA $ \field -> case hfield eqs field of
Dict -> groupByExprOn (`hfield` field)
-- | 'filterWhere' allows an 'Aggregator' to filter out rows from the input
-- query before considering them for aggregation. Note that because the
-- predicate supplied to 'filterWhere' could return 'Rel8.false' for every
-- row, 'filterWhere' needs an 'Aggregator' as opposed to an 'Aggregator1', so
-- that it can return a default value in such a case. For a variant of
-- 'filterWhere' that can work with 'Aggregator1's, see
-- 'Rel8.filterWhereOptional'.
filterWhere :: Table Expr a
=> (i -> Expr Bool) -> Aggregator i a -> Aggregator' fold i a
filterWhere = filterWhereExplicit ifPP
-- | Aggregate rows into a single row containing an array of all aggregated
-- rows. This can be used to associate multiple rows with a single row, without
-- changing the over cardinality of the query. This allows you to essentially
-- return a tree-like structure from queries.
--
-- For example, if we have a table of orders and each orders contains multiple
-- items, we could aggregate the table of orders, pairing each order with its
-- items:
--
-- @
-- ordersWithItems :: Query (Order Expr, ListTable Expr (Item Expr))
-- ordersWithItems = do
-- order <- each orderSchema
-- items <- aggregate listAgg (itemsFromOrder order)
-- return (order, items)
-- @
listAgg :: Table Expr a => Aggregator' fold a (ListTable Expr a)
listAgg =
fromColumns <$>
hvectorizeA \Spec {info} field ->
lmap ((`hfield` field) . toColumns) $ slistAggExpr info
-- | Applies 'listAgg' to the columns selected by the given function.
listAggOn :: Table Expr a => (i -> a) -> Aggregator' fold i (ListTable Expr a)
listAggOn f = lmap f listAgg
-- | Like 'listAgg', but the result is guaranteed to be a non-empty list.
nonEmptyAgg :: Table Expr a => Aggregator1 a (NonEmptyTable Expr a)
nonEmptyAgg =
fromColumns <$>
hvectorizeA \Spec {info} field ->
lmap ((`hfield` field) . toColumns) $ snonEmptyAggExpr info
-- | Applies 'nonEmptyAgg' to the columns selected by the given function.
nonEmptyAggOn :: Table Expr a
=> (i -> a) -> Aggregator1 i (NonEmptyTable Expr a)
nonEmptyAggOn f = lmap f nonEmptyAgg
-- | Concatenate lists into a single list.
listCat :: Table Expr a
=> Aggregator' fold (ListTable Expr a) (ListTable Expr a)
listCat = dimap toColumns fromColumns $
htraverseVectorP (\field -> case hfield hspecs field of
Spec {info} -> slistCatExpr info)
-- | Applies 'listCat' to the list selected by the given function.
listCatOn :: Table Expr a
=> (i -> ListTable Expr a) -> Aggregator' fold i (ListTable Expr a)
listCatOn f = lmap f listCat
-- | Concatenate non-empty lists into a single non-empty list.
nonEmptyCat :: Table Expr a
=> Aggregator1 (NonEmptyTable Expr a) (NonEmptyTable Expr a)
nonEmptyCat = dimap toColumns fromColumns $
htraverseVectorP (\field -> case hfield hspecs field of
Spec {info} -> snonEmptyCatExpr info)
-- | Applies 'nonEmptyCat' to the non-empty list selected by the given
-- function.
nonEmptyCatOn :: Table Expr a
=> (i -> NonEmptyTable Expr a) -> Aggregator1 i (NonEmptyTable Expr a)
nonEmptyCatOn f = lmap f nonEmptyCat
-- | Order the values within each aggregation in an `Aggregator` using the
-- given ordering. This is only relevant for aggregations that depend on the
-- order they get their elements, like `Rel8.listAgg` and `Rel8.stringAgg`.
orderAggregateBy :: Order i -> Aggregator' fold i a -> Aggregator' fold i a
orderAggregateBy (Order order) (Aggregator fallback aggregator) =
Aggregator fallback $ Opaleye.orderAggregate order aggregator