content: {-# 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, quoteArrayElement)
  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 . quoteArrayElement info . toPrimExpr)
      (fromPrimExpr . fromColumn)
      (Fallback (sempty info))
      Opaleye.arrayAgg
  
  
  snonEmptyAggExpr :: ()
    => TypeInformation (Unnullify a) -> Aggregator1 (Expr a) (Expr (NonEmpty a))
  snonEmptyAggExpr info =
    unsafeMakeAggregator
      (toColumn . quoteArrayElement 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
  