rel8-1.5.0.0: src/Rel8/Expr/Aggregate.hs
{-# language DataKinds #-}
{-# language DisambiguateRecordFields #-}
{-# language FlexibleContexts #-}
{-# language NamedFieldPuns #-}
{-# language OverloadedStrings #-}
{-# language ScopedTypeVariables #-}
{-# language TypeApplications #-}
{-# language TypeFamilies #-}
{-# options_ghc -fno-warn-redundant-constraints #-}
module Rel8.Expr.Aggregate
( count, countOn, countStar
, countDistinct, countDistinctOn
, countWhere, countWhereOn
, and, andOn, or, orOn
, min, minOn, max, maxOn
, sum, sumOn, sumWhere
, avg, avgOn
, stringAgg, stringAggOn
, mode, modeOn
, percentile, percentileOn
, percentileContinuous, percentileContinuousOn
, hypotheticalRank
, hypotheticalDenseRank
, hypotheticalPercentRank
, hypotheticalCumeDist
, groupByExpr, groupByExprOn
, distinctAggregate
, filterWhereExplicit
, listAggExpr, listAggExprOn, nonEmptyAggExpr, nonEmptyAggExprOn
, listCatExpr, listCatExprOn, nonEmptyCatExpr, nonEmptyCatExprOn
, slistAggExpr, snonEmptyAggExpr
, slistCatExpr, snonEmptyCatExpr
)
where
-- base
import Data.Functor.Contravariant ((>$<))
import Data.Int ( Int64 )
import Data.List.NonEmpty ( NonEmpty )
import Data.String (IsString)
import Prelude hiding (and, max, min, null, or, show, sum)
-- opaleye
import qualified Opaleye.Aggregate as Opaleye
import qualified Opaleye.Internal.Aggregate as Opaleye
import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
import qualified Opaleye.Internal.Operators as Opaleye
-- profunctors
import Data.Profunctor (dimap, lmap)
-- rel8
import Rel8.Aggregate
( Aggregator' (Aggregator)
, Aggregator1
, filterWhereExplicit
, unsafeMakeAggregator
)
import Rel8.Aggregate.Fold (Fallback (Empty, Fallback))
import Rel8.Expr ( Expr )
import Rel8.Expr.Array (sempty)
import Rel8.Expr.Bool (false, true)
import Rel8.Expr.Eq ((/=.))
import Rel8.Expr.Opaleye
( castExpr
, fromColumn
, fromPrimExpr
, toColumn
, toPrimExpr
, unsafeCastExpr
)
import Rel8.Expr.Order (asc)
import Rel8.Expr.Read (sread)
import Rel8.Expr.Show (show)
import qualified Rel8.Expr.Text as Text
import Rel8.Order (Order (Order))
import Rel8.Schema.Null ( Sql, Unnullify )
import Rel8.Table.Opaleye (fromOrder, unpackspec)
import Rel8.Table.Order (ascTable)
import Rel8.Type ( DBType, typeInformation )
import Rel8.Type.Array (arrayTypeName, encodeArrayElement)
import Rel8.Type.Eq ( DBEq )
import Rel8.Type.Information (TypeInformation)
import Rel8.Type.Num (DBFractional, DBNum)
import Rel8.Type.Ord (DBMax, DBMin, DBOrd)
import Rel8.Type.String ( DBString )
import Rel8.Type.Sum ( DBSum )
-- | Count the occurances of a single column. Corresponds to @COUNT(a)@
count :: Aggregator' fold (Expr a) (Expr Int64)
count =
unsafeMakeAggregator
(toColumn . toPrimExpr)
(fromPrimExpr . fromColumn)
(Fallback 0)
Opaleye.count
-- | Applies 'count' to the column selected by the given function.
countOn :: (i -> Expr a) -> Aggregator' fold i (Expr Int64)
countOn f = lmap f count
-- | Count the number of distinct occurrences of a single column. Corresponds to
-- @COUNT(DISTINCT a)@
countDistinct :: Sql DBEq a
=> Aggregator' fold (Expr a) (Expr Int64)
countDistinct = distinctAggregate count
-- | Applies 'countDistinct' to the column selected by the given function.
countDistinctOn :: Sql DBEq a
=> (i -> Expr a) -> Aggregator' fold i (Expr Int64)
countDistinctOn f = lmap f countDistinct
-- | Corresponds to @COUNT(*)@.
countStar :: Aggregator' fold i (Expr Int64)
countStar = lmap (const true) count
-- | A count of the number of times a given expression is @true@.
countWhere :: Aggregator' fold (Expr Bool) (Expr Int64)
countWhere = filterWhereExplicit ifPP id countStar
-- | Applies 'countWhere' to the column selected by the given function.
countWhereOn :: (i -> Expr Bool) -> Aggregator' fold i (Expr Int64)
countWhereOn f = lmap f countWhere
-- | Corresponds to @bool_and@.
and :: Aggregator' fold (Expr Bool) (Expr Bool)
and =
unsafeMakeAggregator
(toColumn . toPrimExpr)
(fromPrimExpr . fromColumn)
(Fallback true)
Opaleye.boolAnd
-- | Applies 'and' to the column selected by the given function.
andOn :: (i -> Expr Bool) -> Aggregator' fold i (Expr Bool)
andOn f = lmap f and
-- | Corresponds to @bool_or@.
or :: Aggregator' fold (Expr Bool) (Expr Bool)
or =
unsafeMakeAggregator
(toColumn . toPrimExpr)
(fromPrimExpr . fromColumn)
(Fallback false)
Opaleye.boolOr
-- | Applies 'or' to the column selected by the given function.
orOn :: (i -> Expr Bool) -> Aggregator' fold i (Expr Bool)
orOn f = lmap f or
-- | Produce an aggregation for @Expr a@ using the @max@ function.
max :: Sql DBMax a => Aggregator1 (Expr a) (Expr a)
max =
unsafeMakeAggregator
(toColumn . toPrimExpr)
(fromPrimExpr . fromColumn)
Empty
Opaleye.unsafeMax
-- | Applies 'max' to the column selected by the given function.
maxOn :: Sql DBMax a => (i -> Expr a) -> Aggregator1 i (Expr a)
maxOn f = lmap f max
-- | Produce an aggregation for @Expr a@ using the @min@ function.
min :: Sql DBMin a => Aggregator1 (Expr a) (Expr a)
min =
unsafeMakeAggregator
(toColumn . toPrimExpr)
(fromPrimExpr . fromColumn)
Empty
Opaleye.unsafeMin
-- | Applies 'min' to the column selected by the given function.
minOn :: Sql DBMin a => (i -> Expr a) -> Aggregator1 i (Expr a)
minOn f = lmap f min
-- | Corresponds to @sum@. Note that in SQL, @sum@ is type changing - for
-- example the @sum@ of @integer@ returns a @bigint@. Rel8 doesn't support
-- this, and will add explicit casts back to the original input type. This can
-- lead to overflows, and if you anticipate very large sums, you should upcast
-- your input.
sum :: (Sql DBNum a, Sql DBSum a) => Aggregator' fold (Expr a) (Expr a)
sum =
unsafeMakeAggregator
(toColumn . toPrimExpr)
(fromPrimExpr . fromColumn)
(Fallback 0)
Opaleye.unsafeSum
-- | Applies 'sum' to the column selected by the given fucntion.
sumOn :: (Sql DBNum a, Sql DBSum a)
=> (i -> Expr a) -> Aggregator' fold i (Expr a)
sumOn f = lmap f sum
-- | 'sumWhere' is a combination of 'Rel8.filterWhere' and 'sumOn'.
sumWhere :: (Sql DBNum a, Sql DBSum a)
=> (i -> Expr Bool) -> (i -> Expr a) -> Aggregator' fold i (Expr a)
sumWhere condition = filterWhereExplicit ifPP condition . sumOn
-- | Corresponds to @avg@. Note that in SQL, @avg@ is type changing - for
-- example, the @avg@ of @integer@ returns a @numeric@. Rel8 doesn't support
-- this, and will add explicit casts back to the original input type. If you
-- need a fractional result on an integral column, you should cast your input
-- to 'Double' or 'Data.Scientific.Scientific' before calling 'avg'.
avg :: Sql DBSum a => Aggregator1 (Expr a) (Expr a)
avg =
unsafeMakeAggregator
(toColumn . toPrimExpr)
(fromPrimExpr . fromColumn)
Empty
Opaleye.unsafeAvg
-- | Applies 'avg' to the column selected by the given fucntion.
avgOn :: Sql DBSum a => (i -> Expr a) -> Aggregator1 i (Expr a)
avgOn f = lmap f avg
-- | Corresponds to @string_agg()@.
stringAgg :: (Sql IsString a, Sql DBString a)
=> Expr a -> Aggregator' fold (Expr a) (Expr a)
stringAgg delimiter =
unsafeMakeAggregator
(toColumn . toPrimExpr)
(castExpr . fromPrimExpr . fromColumn)
(Fallback "")
(Opaleye.stringAgg (toColumn (toPrimExpr delimiter)))
-- | Applies 'stringAgg' to the column selected by the given function.
stringAggOn :: (Sql IsString a, Sql DBString a)
=> Expr a -> (i -> Expr a) -> Aggregator' fold i (Expr a)
stringAggOn delimiter f = lmap f (stringAgg delimiter)
-- | Corresponds to @mode() WITHIN GROUP (ORDER BY _)@.
mode :: Sql DBOrd a => Aggregator1 (Expr a) (Expr a)
mode =
unsafeMakeAggregator
id
(fromPrimExpr . fromColumn)
Empty
(Opaleye.withinGroup ((\(Order o) -> o) ascTable)
(Opaleye.makeAggrExplicit (pure ()) (Opaleye.AggrOther "mode")))
-- | Applies 'mode' to the column selected by the given function.
modeOn :: Sql DBOrd a => (i -> Expr a) -> Aggregator1 i (Expr a)
modeOn f = lmap f mode
-- | Corresponds to @percentile_disc(_) WITHIN GROUP (ORDER BY _)@.
percentile :: Sql DBOrd a => Expr Double -> Aggregator1 (Expr a) (Expr a)
percentile fraction =
unsafeMakeAggregator
(\a -> (fraction, a))
(castExpr . fromPrimExpr . fromColumn)
Empty
(Opaleye.withinGroup ((\(Order o) -> o) (snd >$< ascTable))
(Opaleye.makeAggrExplicit
(lmap fst unpackspec)
(Opaleye.AggrOther "percentile_disc")))
-- | Applies 'percentile' to the column selected by the given function.
percentileOn ::
Sql DBOrd a =>
Expr Double ->
(i -> Expr a) ->
Aggregator1 i (Expr a)
percentileOn fraction f = lmap f (percentile fraction)
-- | Corresponds to @percentile_cont(_) WITHIN GROUP (ORDER BY _)@.
percentileContinuous ::
Sql DBFractional a =>
Expr Double ->
Aggregator1 (Expr a) (Expr a)
percentileContinuous fraction =
unsafeMakeAggregator
(\a -> (fraction, a))
(castExpr . fromPrimExpr . fromColumn)
Empty
(Opaleye.withinGroup ((\(Order o) -> o) (unsafeCastExpr @Double . snd >$< asc))
(Opaleye.makeAggrExplicit
(lmap fst unpackspec)
(Opaleye.AggrOther "percentile_disc")))
-- | Applies 'percentileContinuous' to the column selected by the given
-- function.
percentileContinuousOn ::
Sql DBFractional a =>
Expr Double ->
(i -> Expr a) ->
Aggregator1 i (Expr a)
percentileContinuousOn fraction f = lmap f (percentileContinuous fraction)
-- | Corresponds to @rank(_) WITHIN GROUP (ORDER BY _)@.
hypotheticalRank ::
Order a ->
a ->
Aggregator' fold a (Expr Int64)
hypotheticalRank (Order order) args =
unsafeMakeAggregator
(\a -> (args, a))
(castExpr . fromPrimExpr . fromColumn)
(Fallback 1)
(Opaleye.withinGroup (snd >$< order)
(Opaleye.makeAggrExplicit
(fromOrder (fst >$< order))
(Opaleye.AggrOther "rank")))
-- | Corresponds to @dense_rank(_) WITHIN GROUP (ORDER BY _)@.
hypotheticalDenseRank ::
Order a ->
a ->
Aggregator' fold a (Expr Int64)
hypotheticalDenseRank (Order order) args =
unsafeMakeAggregator
(const args)
(castExpr . fromPrimExpr . fromColumn)
(Fallback 1)
(Opaleye.withinGroup order
(Opaleye.makeAggrExplicit (fromOrder order)
(Opaleye.AggrOther "dense_rank")))
-- | Corresponds to @percent_rank(_) WITHIN GROUP (ORDER BY _)@.
hypotheticalPercentRank ::
Order a ->
a ->
Aggregator' fold a (Expr Double)
hypotheticalPercentRank (Order order) args =
unsafeMakeAggregator
(const args)
(castExpr . fromPrimExpr . fromColumn)
(Fallback 0)
(Opaleye.withinGroup order
(Opaleye.makeAggrExplicit (fromOrder order)
(Opaleye.AggrOther "percent_rank")))
-- | Corresponds to @cume_dist(_) WITHIN GROUP (ORDER BY _)@.
hypotheticalCumeDist ::
Order a ->
a ->
Aggregator' fold a (Expr Double)
hypotheticalCumeDist (Order order) args =
unsafeMakeAggregator
(const args)
(castExpr . fromPrimExpr . fromColumn)
(Fallback 1)
(Opaleye.withinGroup order
(Opaleye.makeAggrExplicit (fromOrder order)
(Opaleye.AggrOther "cume_dist")))
-- | Aggregate a value by grouping by it.
groupByExpr :: Sql DBEq a => Aggregator1 (Expr a) (Expr a)
groupByExpr =
unsafeMakeAggregator
(toColumn . toPrimExpr)
(fromPrimExpr . fromColumn)
Empty
Opaleye.groupBy
-- | Applies 'groupByExpr' to the column selected by the given function.
groupByExprOn :: Sql DBEq a => (i -> Expr a) -> Aggregator1 i (Expr a)
groupByExprOn f = lmap f groupByExpr
-- | Collect expressions values as a list.
listAggExpr :: Sql DBType a => Aggregator' fold (Expr a) (Expr [a])
listAggExpr = slistAggExpr typeInformation
-- | Applies 'listAggExpr' to the column selected by the given function.
listAggExprOn :: Sql DBType a => (i -> Expr a) -> Aggregator' fold i (Expr [a])
listAggExprOn f = lmap f listAggExpr
-- | Collect expressions values as a non-empty list.
nonEmptyAggExpr :: Sql DBType a => Aggregator1 (Expr a) (Expr (NonEmpty a))
nonEmptyAggExpr = snonEmptyAggExpr typeInformation
-- | Applies 'nonEmptyAggExpr' to the column selected by the given function.
nonEmptyAggExprOn :: Sql DBType a
=> (i -> Expr a) -> Aggregator1 i (Expr (NonEmpty a))
nonEmptyAggExprOn f = lmap f nonEmptyAggExpr
-- | Concatenate lists into a single list.
listCatExpr :: Sql DBType a => Aggregator' fold (Expr [a]) (Expr [a])
listCatExpr = slistCatExpr typeInformation
-- | Applies 'listCatExpr' to the column selected by the given function.
listCatExprOn :: Sql DBType a
=> (i -> Expr [a]) -> Aggregator' fold i (Expr [a])
listCatExprOn f = lmap f listCatExpr
-- | Concatenate non-empty lists into a single non-empty list.
nonEmptyCatExpr :: Sql DBType a
=> Aggregator1 (Expr (NonEmpty a)) (Expr (NonEmpty a))
nonEmptyCatExpr = snonEmptyCatExpr typeInformation
-- | Applies 'nonEmptyCatExpr' to the column selected by the given function.
nonEmptyCatExprOn :: Sql DBType a
=> (i -> Expr (NonEmpty a)) -> Aggregator1 i (Expr (NonEmpty a))
nonEmptyCatExprOn f = lmap f nonEmptyCatExpr
-- | 'distinctAggregate' modifies an 'Aggregator' to consider only distinct
-- values of each particular column. Note that this "distinction" only happens
-- within each column individually, not across all columns simultaneously.
distinctAggregate :: Aggregator' fold i a -> Aggregator' fold i a
distinctAggregate (Aggregator fallback a) =
Aggregator fallback (Opaleye.distinctAggregator a)
slistAggExpr :: ()
=> TypeInformation (Unnullify a) -> Aggregator' fold (Expr a) (Expr [a])
slistAggExpr info =
unsafeMakeAggregator
(toColumn . encodeArrayElement info . toPrimExpr)
(fromPrimExpr . fromColumn)
(Fallback (sempty info))
Opaleye.arrayAgg
snonEmptyAggExpr :: ()
=> TypeInformation (Unnullify a) -> Aggregator1 (Expr a) (Expr (NonEmpty a))
snonEmptyAggExpr info =
unsafeMakeAggregator
(toColumn . encodeArrayElement info . toPrimExpr)
(fromPrimExpr . fromColumn)
Empty
Opaleye.arrayAgg
slistCatExpr :: ()
=> TypeInformation (Unnullify a) -> Aggregator' fold (Expr [a]) (Expr [a])
slistCatExpr info = dimap (unbracket . show) (sread name . bracket) agg
where
bracket a = "{" <> a <> "}"
unbracket a = Text.substr a 2 (Just (Text.length a - 2))
agg = filterWhereExplicit ifPP (/=. "") (stringAgg ",")
name = arrayTypeName info
snonEmptyCatExpr :: ()
=> TypeInformation (Unnullify a)
-> Aggregator1 (Expr (NonEmpty a)) (Expr (NonEmpty a))
snonEmptyCatExpr info = dimap (unbracket . show) (sread name . bracket) agg
where
bracket a = "{" <> a <> "}"
unbracket a = Text.substr a 2 (Just (Text.length a - 2))
agg = filterWhereExplicit ifPP (/=. "") (stringAgg ",")
name = arrayTypeName info
ifPP :: Opaleye.IfPP (Expr a) (Expr a)
ifPP = dimap from to Opaleye.ifPPField
where
from = toColumn . toPrimExpr
to = fromPrimExpr . fromColumn