content: {-# language DerivingVia #-}
  {-# language FlexibleContexts #-}
  {-# language GeneralizedNewtypeDeriving #-}
  {-# language ScopedTypeVariables #-}
  {-# language StandaloneKindSignatures #-}
  {-# language TypeApplications #-}
  
  module Rel8.Window
    ( Window(..)
    , Partition
    , over
    , partitionBy
    , orderPartitionBy
    )
  where
  
  -- base
  import Data.Functor.Const ( Const( Const ), getConst )
  import Data.Functor.Contravariant ( Contravariant, contramap )
  import Data.Kind ( Type )
  import Prelude
  
  -- opaleye
  import qualified Opaleye.Internal.Window as Opaleye
  import qualified Opaleye.Internal.PackMap as Opaleye
  
  -- profunctors
  import Data.Profunctor ( Profunctor )
  
  -- product-profunctors
  import Data.Profunctor.Product ( ProductProfunctor, (****), purePP )
  
  -- rel8
  import Rel8.Expr.Opaleye ( toColumn, toPrimExpr )
  import Rel8.Order( Order( Order ) )
  import Rel8.Schema.HTable ( hfield, htabulateA )
  import Rel8.Table ( Columns, toColumns )
  import Rel8.Table.Eq ( EqTable )
  
  -- semigroupoids
  import Data.Functor.Apply ( Apply, WrappedApplicative(..) )
  
  
  -- | 'Window' is an applicative functor that represents expressions that
  -- contain
  -- [window functions](https://www.postgresql.org/docs/current/tutorial-window.html).
  -- 'Rel8.Query.Window.window' can be used to
  -- evaluate these expressions over a particular query.
  type Window :: Type -> Type -> Type
  newtype Window a b = Window (Opaleye.Windows a b)
    deriving newtype (Profunctor)
    deriving newtype (Functor, Applicative)
    deriving (Apply) via (WrappedApplicative (Window a))
  
  
  instance ProductProfunctor Window where
    purePP = pure
    (****) = (<*>)
  
  
  -- | In PostgreSQL, window functions must specify the \"window\" or
  -- \"partition\" over which they operate. The syntax for this looks like:
  -- @SUM(salary) OVER (PARTITION BY department)@. The Rel8 type 'Partition'
  -- represents everything that comes after @OVER@.
  --
  -- 'Partition' is a 'Monoid', so 'Window's created with 'partitionBy' and
  -- 'orderWindowBy' can be combined using '<>'.
  type Partition :: Type -> Type
  newtype Partition a = Partition (Opaleye.Window a)
    deriving newtype (Contravariant, Semigroup, Monoid)
  
  
  -- | 'over' adds a 'Partition' to a 'Window' expression.
  --
  -- @
  --   'Rel8.Table.Window.cumulative' ('Rel8.Expr.Aggregate.sum' . salary) `over` 'partitionBy' department <> 'orderPartitionBy' (salary >$< 'Rel8.desc')
  -- @
  over :: Window a b -> Partition a -> Window a b
  over (Window (Opaleye.Windows (Opaleye.PackMap w))) (Partition p) =
    Window $ Opaleye.Windows $ Opaleye.PackMap $ \f ->
      w (\(o, p') -> f (o, p' <> p))
  infixl 1 `over`
  
  
  -- | Restricts a window function to operate only the group of rows that share
  -- the same value(s) for the given expression(s).
  partitionBy :: forall b a. EqTable b => (a -> b) -> Partition a
  partitionBy f =
    Partition $ contramap (toColumns . f) $ getConst $
      htabulateA @(Columns b) $ \field ->
        Const $ Opaleye.partitionBy (toColumn . toPrimExpr . (`hfield` field))
  
  
  -- | Controls the order in which rows are processed by window functions. This
  -- does not need to match the ordering of the overall query.
  orderPartitionBy :: Order a -> Partition a
  orderPartitionBy (Order ordering) = Partition $ Opaleye.orderPartitionBy ordering
  