packages feed

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

-- https://www.postgresql.org/docs/current/tutorial-window.html#id-1.4.5.6.9.5
-- talks about partitions and window frames.  The window frame is the
-- way the elements of a partition are ordered for processing the
-- result row of each element of the partition.
--
-- So neither of these terms is suitable for the _whole thing_.
-- Perhaps the answer should be "Window"?  This is also attested by
-- the WINDOW declaration in a SELECT.

module Opaleye.Internal.Window where

import           Control.Applicative (Applicative, pure, (<*>), liftA2)
import           Data.Profunctor (lmap, Profunctor, dimap)
import           Data.Semigroup (Semigroup, (<>))

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
import Data.Functor.Contravariant (contramap, Contravariant)
import Control.Arrow (second)


-- | 'WindowFunction' represents expressions that contain [window
-- functions](https://www.postgresql.org/docs/current/tutorial-window.html).
-- You can choose a 'WindowFunction' from the options below, and
-- combine and manipulate them using the @Applicative@ and
-- 'Data.Profunctor.Profunctor' operations.
newtype WindowFunction a b =
  WindowFunction (PM.PackMap HPQ.WndwOp HPQ.PrimExpr a b)

instance Functor (WindowFunction a) where
  fmap f (WindowFunction w) = WindowFunction (fmap f w)

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

instance Profunctor WindowFunction where
  dimap f g (WindowFunction w) =  WindowFunction (dimap f g w)

-- | You can create @Windows@ using 'over', and combine and manipulate
-- them using the @Applicative@ and 'Data.Profunctor.Profunctor'
-- operations.
newtype Windows a b =
  Windows (PM.PackMap (HPQ.WndwOp, Window a) HPQ.PrimExpr a b)

instance Functor (Windows a) where
  fmap f (Windows w) = Windows (fmap f w)

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

instance Profunctor Windows where
  dimap f g (Windows (PM.PackMap pm)) =
    Windows $ PM.PackMap $ \h a ->
      fmap g (pm (\(op, w) -> h (op, contramap f w)) (f a))

runWindows' :: Applicative f
  => Windows a b -> ((HPQ.WndwOp, Window a) -> f HPQ.PrimExpr) -> a -> f b
runWindows' (Windows a) = PM.traversePM a


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


-- | A 'WindowFunction' that doesn't actually contain any window
-- function.
noWindowFunction :: (a -> b) -> WindowFunction a b
noWindowFunction f = fmap f (WindowFunction (PM.PackMap (const pure)))


-- | @runWindows@ runs a query composed of expressions containing
-- [window
-- functions](https://www.postgresql.org/docs/current/tutorial-window.html).
-- @runWindows@ 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 per group. In Haskell
-- terminology, 'Opaleye.aggregate' is to 'foldl' as @runWindows@ is
-- to 'scanl'.
runWindows :: Windows a b -> Q.Select a -> Q.Select b
runWindows wndw q = Q.productQueryArr $ do
  (a, primQ) <- Q.runSimpleSelect q
  tag <- T.fresh
  let
    (b, bindings) = PM.run (runWindows' wndw (extractWindowFields tag a) a)
  pure (b, PQ.Window bindings primQ)


windowsApply :: Windows (Windows a b, a) b
windowsApply = Windows $ PM.PackMap $ \f (agg, a) ->
  case agg of
    Windows (PM.PackMap inner) -> inner (f . second (contramap snd)) a


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


makeWndwField :: (HPQ.PrimExpr -> HPQ.WndwOp)
              -> WindowFunction (C.Field_ n a) (C.Field_ n' a')
makeWndwField f = lmap (f . C.unColumn) makeWndw


makeWndwAny :: HPQ.WndwOp -> WindowFunction a (C.Field_ n b)
makeWndwAny op = lmap (const op) makeWndw

-- | 'aggregatorWindowFunction' allows the use of 'A.Aggregator's in
-- 'WindowFunction's. In particular, @'aggregatorWindowFunction'
-- 'Opaleye.sum'@ gives a running total (when combined with an order
-- argument to 'over').
aggregatorWindowFunction :: A.Aggregator a b -> (a' -> a) -> WindowFunction a' b
aggregatorWindowFunction agg g = WindowFunction $ PM.PackMap $ \f a ->
  pm (\(mop, expr) -> case mop of
         HPQ.GroupBy -> pure expr
         HPQ.Aggr op _ _ _ -> f (HPQ.WndwAggregate op expr)) a
  where A.Aggregator (PM.PackMap pm) = lmap g agg


-- | 'over' applies a 'WindowFunction' on a particular 'Window'.  For
-- example,
--
-- @
-- over ('aggregatorWindowFunction' 'Opaleye.sum' salary) ('partitionBy' department) ('Opaleye.desc' salary)
-- @
--
-- If you want to use a 'Window' that consists of the entire @SELECT@
-- then supply 'mempty' for the @'Window' a@ argument.  If you don't
-- want to order the 'Window' then supply 'mempty' for the @'O.Order'
-- a@ argument.
over :: WindowFunction a b -> Window a -> O.Order a -> Windows a b
over (WindowFunction windowFunction) partition order =
  let PM.PackMap pm = windowFunction
      orderPartitionBy' = orderPartitionBy order
  in Windows $ PM.PackMap $ \f -> pm (\op ->
    f (op, partition <> orderPartitionBy'))

-- | In PostgreSQL, window functions must specify the \"window\" over
-- which they operate. The syntax for this looks like: @SUM(salary)
-- OVER (PARTITION BY department)@. The Opaleye type 'Window'
-- represents the segment consisting of the @PARTIION BY@.
--
-- You can create a @Window@ using 'partitionBy' and combine two
-- @Windows@ in a single one which combines the partition of both by
-- using '<>'.
data Window a = Window (a -> [HPQ.PrimExpr]) (O.Order a)

instance Semigroup (Window a) where
  Window p1 o1 <> Window p2 o2 = Window (p1 <> p2) (o1 <> o2)

instance Monoid (Window a) where
  mempty = Window mempty mempty
  mappend = (<>)

instance Contravariant Window where
  contramap f (Window p o) = Window (lmap f p) (contramap f o)

-- | The window where each partition shares the same value for the
-- given 'Field'.
partitionBy :: (a -> C.Field_ n b) -> Window a
partitionBy f = Window (\a -> [C.unColumn (f a)]) mempty

-- | Controls the order in which rows are processed by window functions. This
-- does not need to match the ordering of the overall query.
orderPartitionBy :: O.Order a -> Window a
orderPartitionBy = Window mempty