packages feed

opaleye-0.9.3.3: src/Opaleye/Internal/Window.hs

module Opaleye.Internal.Window where

import           Control.Applicative (Applicative, pure, (<*>))

import qualified Opaleye.Internal.Aggregate as A
import qualified Opaleye.Internal.PackMap as PM
import qualified Opaleye.Internal.PrimQuery as PQ
import qualified Opaleye.Internal.QueryArr as Q
import qualified Opaleye.Internal.Tag as T
import qualified Opaleye.Internal.Column as C
import qualified Opaleye.Internal.Order as O

import qualified Opaleye.Internal.HaskellDB.PrimQuery as HPQ


-- | 'Window' is an applicative functor that represents expressions that
-- contain
-- [window functions](https://www.postgresql.org/docs/current/tutorial-window.html).
-- 'window' can be used to evaluate these expressions over a particular query.
newtype Window a =
  Window (PM.PackMap (HPQ.WndwOp, Partition) HPQ.PrimExpr () a)


instance Functor Window where
  fmap f (Window g) = Window (fmap f g)


instance Applicative Window where
  pure = Window . pure
  Window f <*> Window x = Window (f <*> x)


runWindow :: Applicative f
  => Window a -> ((HPQ.WndwOp, Partition) -> f HPQ.PrimExpr) -> f a
runWindow (Window a) f = PM.traversePM a f ()


extractWindowFields
  :: T.Tag
  -> (HPQ.WndwOp, Partition)
  -> PM.PM (PQ.Bindings (HPQ.WndwOp, HPQ.Partition)) HPQ.PrimExpr
extractWindowFields tag (op, Partition ps os) = do
  i <- PM.new
  let symbol = HPQ.Symbol ("window" ++ i) tag
  PM.write (symbol, (op, (HPQ.Partition ps os)))
  pure (HPQ.AttrExpr symbol)


-- | 'window' runs a query composed of expressions containing
-- [window functions](https://www.postgresql.org/docs/current/tutorial-window.html).
-- 'window' is similar to 'Opaleye.aggregate', with the main difference being
-- that in a window query, each input row corresponds to one output row,
-- whereas aggregation queries fold the entire input query down into a single
-- row. To put this into a Haskell context, 'Opaleye.aggregate' is to 'foldl'
-- as 'window' is to 'scanl'.
window :: Q.Select (Window a) -> Q.Select a
window q = Q.productQueryArr $ do
  (wndw, primQ) <- Q.runSimpleQueryArr' q ()
  tag <- T.fresh
  let
    (a, bindings) = PM.run (runWindow wndw (extractWindowFields tag))
  pure (a, PQ.Window bindings primQ)


makeWndw :: HPQ.WndwOp -> Window (C.Field_ n a)
makeWndw op = Window (PM.PackMap (\f _ -> C.Column <$> f (op, mempty)))


-- | 'cumulative' allows the use of aggregation functions in 'Window'
-- expressions. In particular, @'cumulative' 'Opaleye.sum'@
-- (when combined with 'orderPartitionBy') gives a running total,
-- also known as a \"cumulative sum\", hence the name @cumulative@.
cumulative :: A.Aggregator a b -> a -> Window b
cumulative (A.Aggregator (PM.PackMap pm)) a = Window $ PM.PackMap $ \f _ ->
  pm (\(mop, expr) -> case mop of
         Nothing -> pure expr
         Just (op, _, _) -> f (HPQ.WndwAggregate op expr, mempty)) a


-- | 'over' adds a 'Partition' to a 'Window' expression.
--
-- @
-- 'cumulative' 'Opaleye.sum' salary \`'over'\` 'partitionBy' department <> 'orderPartitionBy' salary ('Opaleye.desc' id)
-- @
over :: Window a -> Partition -> Window a
over (Window (PM.PackMap pm)) partition =
  Window $ PM.PackMap $ \f -> pm $ \(op, partition') ->
    f (op, partition' <> partition)
infixl 1 `over`


-- | 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 Opaleye type 'Partition'
-- represents everything that comes after @OVER@.
--
-- 'Partition' is a 'Monoid', so 'Partition's created with 'partitionBy' and
-- 'orderPartitionBy' can be combined using '<>'.
data Partition = Partition ![HPQ.PrimExpr] ![HPQ.OrderExpr]


instance Semigroup Partition where
  Partition ps os <> Partition ps' os' = Partition (ps <> ps') (os <> os')


instance Monoid Partition where
  mempty = Partition [] []


-- | Restricts a window function to operate only the group of rows that share
-- the same value(s) for the given expression(s).
partitionBy :: C.Field_ n a -> Partition
partitionBy (C.Column expr) = Partition [expr] []


-- | Controls the order in which rows are processed by window functions. This
-- does not need to match the ordering of the overall query.
orderPartitionBy :: a -> O.Order a -> Partition
orderPartitionBy a ordering = Partition [] (O.orderExprs a ordering)