opaleye 0.9.5.1 → 0.9.6.0
raw patch · 12 files changed
+393/−2 lines, 12 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
+ Opaleye.Internal.HaskellDB.PrimQuery: Partition :: [PrimExpr] -> [OrderExpr] -> Partition
+ Opaleye.Internal.HaskellDB.PrimQuery: WndwAggregate :: AggrOp -> PrimExpr -> WndwOp
+ Opaleye.Internal.HaskellDB.PrimQuery: WndwCumeDist :: WndwOp
+ Opaleye.Internal.HaskellDB.PrimQuery: WndwDenseRank :: WndwOp
+ Opaleye.Internal.HaskellDB.PrimQuery: WndwExpr :: WndwOp -> Partition -> PrimExpr
+ Opaleye.Internal.HaskellDB.PrimQuery: WndwFirstValue :: PrimExpr -> WndwOp
+ Opaleye.Internal.HaskellDB.PrimQuery: WndwLag :: PrimExpr -> PrimExpr -> PrimExpr -> WndwOp
+ Opaleye.Internal.HaskellDB.PrimQuery: WndwLastValue :: PrimExpr -> WndwOp
+ Opaleye.Internal.HaskellDB.PrimQuery: WndwLead :: PrimExpr -> PrimExpr -> PrimExpr -> WndwOp
+ Opaleye.Internal.HaskellDB.PrimQuery: WndwNthValue :: PrimExpr -> PrimExpr -> WndwOp
+ Opaleye.Internal.HaskellDB.PrimQuery: WndwNtile :: PrimExpr -> WndwOp
+ Opaleye.Internal.HaskellDB.PrimQuery: WndwPercentRank :: WndwOp
+ Opaleye.Internal.HaskellDB.PrimQuery: WndwRank :: WndwOp
+ Opaleye.Internal.HaskellDB.PrimQuery: WndwRowNumber :: WndwOp
+ Opaleye.Internal.HaskellDB.PrimQuery: [orderBy] :: Partition -> [OrderExpr]
+ Opaleye.Internal.HaskellDB.PrimQuery: [partitionBy] :: Partition -> [PrimExpr]
+ Opaleye.Internal.HaskellDB.PrimQuery: data Partition
+ Opaleye.Internal.HaskellDB.PrimQuery: data WndwOp
+ Opaleye.Internal.HaskellDB.PrimQuery: instance GHC.Read.Read Opaleye.Internal.HaskellDB.PrimQuery.Partition
+ Opaleye.Internal.HaskellDB.PrimQuery: instance GHC.Read.Read Opaleye.Internal.HaskellDB.PrimQuery.WndwOp
+ Opaleye.Internal.HaskellDB.PrimQuery: instance GHC.Show.Show Opaleye.Internal.HaskellDB.PrimQuery.Partition
+ Opaleye.Internal.HaskellDB.PrimQuery: instance GHC.Show.Show Opaleye.Internal.HaskellDB.PrimQuery.WndwOp
+ Opaleye.Internal.HaskellDB.Sql: SqlPartition :: Maybe (NonEmpty SqlExpr) -> Maybe (NonEmpty (SqlExpr, SqlOrder)) -> SqlPartition
+ Opaleye.Internal.HaskellDB.Sql: WndwFunSqlExpr :: String -> [SqlExpr] -> SqlPartition -> SqlExpr
+ Opaleye.Internal.HaskellDB.Sql: [sqlOrderBy] :: SqlPartition -> Maybe (NonEmpty (SqlExpr, SqlOrder))
+ Opaleye.Internal.HaskellDB.Sql: [sqlPartitionBy] :: SqlPartition -> Maybe (NonEmpty SqlExpr)
+ Opaleye.Internal.HaskellDB.Sql: data SqlPartition
+ Opaleye.Internal.HaskellDB.Sql: instance GHC.Show.Show Opaleye.Internal.HaskellDB.Sql.SqlPartition
+ Opaleye.Internal.HaskellDB.Sql.Default: showWndwOp :: SqlGenerator -> WndwOp -> (String, [SqlExpr])
+ Opaleye.Internal.HaskellDB.Sql.Default: toSqlPartition :: SqlGenerator -> Partition -> SqlPartition
+ Opaleye.Internal.PrimQuery: Window :: Bindings (WndwOp, Partition) -> PrimQuery' a -> PrimQuery' a
+ Opaleye.Internal.PrimQuery: [window] :: PrimQueryFoldP a p p' -> Bindings (WndwOp, Partition) -> p -> p'
+ Opaleye.Internal.Sql: window :: Bindings (WndwOp, Partition) -> Select -> Select
+ Opaleye.Internal.Window: Window :: (a -> [PrimExpr]) -> Order a -> Window a
+ Opaleye.Internal.Window: WindowFunction :: PackMap WndwOp PrimExpr a b -> WindowFunction a b
+ Opaleye.Internal.Window: Windows :: PackMap (WndwOp, Window a) PrimExpr a b -> Windows a b
+ Opaleye.Internal.Window: aggregatorWindowFunction :: Aggregator a b -> (a' -> a) -> WindowFunction a' b
+ Opaleye.Internal.Window: data Window a
+ Opaleye.Internal.Window: extractWindowFields :: Tag -> a -> (WndwOp, Window a) -> PM (Bindings (WndwOp, Partition)) PrimExpr
+ Opaleye.Internal.Window: instance Data.Functor.Contravariant.Contravariant Opaleye.Internal.Window.Window
+ Opaleye.Internal.Window: instance Data.Profunctor.Unsafe.Profunctor Opaleye.Internal.Window.WindowFunction
+ Opaleye.Internal.Window: instance Data.Profunctor.Unsafe.Profunctor Opaleye.Internal.Window.Windows
+ Opaleye.Internal.Window: instance GHC.Base.Applicative (Opaleye.Internal.Window.WindowFunction a)
+ Opaleye.Internal.Window: instance GHC.Base.Applicative (Opaleye.Internal.Window.Windows a)
+ Opaleye.Internal.Window: instance GHC.Base.Functor (Opaleye.Internal.Window.WindowFunction a)
+ Opaleye.Internal.Window: instance GHC.Base.Functor (Opaleye.Internal.Window.Windows a)
+ Opaleye.Internal.Window: instance GHC.Base.Monoid (Opaleye.Internal.Window.Window a)
+ Opaleye.Internal.Window: instance GHC.Base.Semigroup (Opaleye.Internal.Window.Window a)
+ Opaleye.Internal.Window: makeWndw :: WindowFunction WndwOp (Field_ n a)
+ Opaleye.Internal.Window: makeWndwAny :: WndwOp -> WindowFunction a (Field_ n b)
+ Opaleye.Internal.Window: makeWndwField :: (PrimExpr -> WndwOp) -> WindowFunction (Field_ n a) (Field_ n' a')
+ Opaleye.Internal.Window: newtype WindowFunction a b
+ Opaleye.Internal.Window: newtype Windows a b
+ Opaleye.Internal.Window: noWindowFunction :: (a -> b) -> WindowFunction a b
+ Opaleye.Internal.Window: orderPartitionBy :: Order a -> Window a
+ Opaleye.Internal.Window: over :: WindowFunction a b -> Window a -> Order a -> Windows a b
+ Opaleye.Internal.Window: partitionBy :: (a -> Field_ n b) -> Window a
+ Opaleye.Internal.Window: runWindows :: Windows a b -> Select a -> Select b
+ Opaleye.Internal.Window: runWindows' :: Applicative f => Windows a b -> ((WndwOp, Window a) -> f PrimExpr) -> a -> f b
+ Opaleye.Internal.Window: windowsApply :: Windows (Windows a b, a) b
+ Opaleye.Window: aggregatorWindowFunction :: Aggregator a b -> (a' -> a) -> WindowFunction a' b
+ Opaleye.Window: cumeDist :: WindowFunction a (Field SqlFloat8)
+ Opaleye.Window: data Window a
+ Opaleye.Window: data WindowFunction a b
+ Opaleye.Window: data Windows a b
+ Opaleye.Window: denseRank :: WindowFunction a (Field SqlInt8)
+ Opaleye.Window: firstValue :: WindowFunction (Field_ n a) (Field_ n a)
+ Opaleye.Window: lag :: Field SqlInt4 -> Field_ n a -> WindowFunction (Field_ n a) (Field_ n a)
+ Opaleye.Window: lastValue :: WindowFunction (Field_ n a) (Field_ n a)
+ Opaleye.Window: lead :: Field SqlInt4 -> Field_ n a -> WindowFunction (Field_ n a) (Field_ n a)
+ Opaleye.Window: noWindowFunction :: (a -> b) -> WindowFunction a b
+ Opaleye.Window: nthValue :: Field SqlInt4 -> WindowFunction (Field_ n a) (FieldNullable a)
+ Opaleye.Window: ntile :: Field SqlInt4 -> WindowFunction a (Field SqlInt4)
+ Opaleye.Window: over :: WindowFunction a b -> Window a -> Order a -> Windows a b
+ Opaleye.Window: partitionBy :: (a -> Field_ n b) -> Window a
+ Opaleye.Window: percentRank :: WindowFunction a (Field SqlFloat8)
+ Opaleye.Window: rank :: WindowFunction a (Field SqlInt8)
+ Opaleye.Window: rowNumber :: WindowFunction a (Field SqlInt8)
+ Opaleye.Window: runWindows :: Windows a b -> Select a -> Select b
- Opaleye.Internal.PrimQuery: PrimQueryFold :: p' -> (a -> p') -> (TableIdentifier -> Bindings PrimExpr -> p') -> (NonEmpty (Lateral, p) -> [PrimExpr] -> p') -> (Bindings (Maybe (AggrOp, [OrderExpr], AggrDistinct), Symbol) -> p -> p') -> (Maybe (NonEmpty PrimExpr) -> [OrderExpr] -> p -> p') -> (LimitOp -> p -> p') -> (JoinType -> PrimExpr -> (Lateral, p) -> (Lateral, p) -> p') -> (SemijoinType -> p -> p -> p') -> (Symbol -> p -> p') -> ([Symbol] -> NonEmpty [PrimExpr] -> p') -> (BinOp -> (p, p) -> p') -> (String -> p -> p') -> (PrimExpr -> Bindings PrimExpr -> p') -> (Bool -> Bindings PrimExpr -> p -> p') -> (p -> p') -> (Recursive -> Symbol -> [Symbol] -> p -> p -> p') -> PrimQueryFoldP a p p'
+ Opaleye.Internal.PrimQuery: PrimQueryFold :: p' -> (a -> p') -> (TableIdentifier -> Bindings PrimExpr -> p') -> (NonEmpty (Lateral, p) -> [PrimExpr] -> p') -> (Bindings (Maybe (AggrOp, [OrderExpr], AggrDistinct), Symbol) -> p -> p') -> (Bindings (WndwOp, Partition) -> p -> p') -> (Maybe (NonEmpty PrimExpr) -> [OrderExpr] -> p -> p') -> (LimitOp -> p -> p') -> (JoinType -> PrimExpr -> (Lateral, p) -> (Lateral, p) -> p') -> (SemijoinType -> p -> p -> p') -> (Symbol -> p -> p') -> ([Symbol] -> NonEmpty [PrimExpr] -> p') -> (BinOp -> (p, p) -> p') -> (String -> p -> p') -> (PrimExpr -> Bindings PrimExpr -> p') -> (Bool -> Bindings PrimExpr -> p -> p') -> (p -> p') -> (Recursive -> Symbol -> [Symbol] -> p -> p -> p') -> PrimQueryFoldP a p p'
Files
- CHANGELOG.md +5/−0
- opaleye.cabal +3/−1
- src/Opaleye.hs +2/−0
- src/Opaleye/Internal/HaskellDB/PrimQuery.hs +22/−0
- src/Opaleye/Internal/HaskellDB/Sql.hs +8/−0
- src/Opaleye/Internal/HaskellDB/Sql/Default.hs +26/−0
- src/Opaleye/Internal/HaskellDB/Sql/Print.hs +18/−1
- src/Opaleye/Internal/Optimize.hs +1/−0
- src/Opaleye/Internal/PrimQuery.hs +5/−0
- src/Opaleye/Internal/Sql.hs +7/−0
- src/Opaleye/Internal/Window.hs +182/−0
- src/Opaleye/Window.hs +114/−0
CHANGELOG.md view
@@ -1,3 +1,8 @@+## 0.9.6.0++* Add `Opaleye.Window` to support window functions. Thanks to Shane+ O'Brien.+ ## 0.9.5.1 * Actually expose `arrayAgg_`
opaleye.cabal view
@@ -1,6 +1,6 @@ name: opaleye copyright: Copyright (c) 2014-2018 Purely Agile Limited; 2019-2022 Tom Ellis-version: 0.9.5.1+version: 0.9.6.0 synopsis: An SQL-generating DSL targeting PostgreSQL description: An SQL-generating DSL targeting PostgreSQL. Allows Postgres queries to be written within Haskell in a@@ -70,6 +70,7 @@ Opaleye.TypeFamilies, Opaleye.Values, Opaleye.With,+ Opaleye.Window, Opaleye.Internal.Aggregate, Opaleye.Internal.Binary, Opaleye.Internal.Constant,@@ -103,6 +104,7 @@ Opaleye.Internal.TypeFamilies, Opaleye.Internal.Unpackspec, Opaleye.Internal.Values,+ Opaleye.Internal.Window, Opaleye.Internal.HaskellDB.PrimQuery, Opaleye.Internal.HaskellDB.Sql, Opaleye.Internal.HaskellDB.Sql.Default,
src/Opaleye.hs view
@@ -37,6 +37,7 @@ , module Opaleye.ToFields , module Opaleye.Values , module Opaleye.With+ , module Opaleye.Window ) where import Opaleye.Adaptors@@ -62,4 +63,5 @@ import Opaleye.Table import Opaleye.ToFields import Opaleye.Values+import Opaleye.Window import Opaleye.With
src/Opaleye/Internal/HaskellDB/PrimQuery.hs view
@@ -23,6 +23,7 @@ | BinExpr BinOp PrimExpr PrimExpr | UnExpr UnOp PrimExpr | AggrExpr AggrDistinct AggrOp PrimExpr [OrderExpr]+ | WndwExpr WndwOp Partition | ConstExpr Literal | CaseExpr [(PrimExpr,PrimExpr)] PrimExpr | ListExpr (NEL.NonEmpty PrimExpr)@@ -101,3 +102,24 @@ data BoundExpr = Inclusive PrimExpr | Exclusive PrimExpr | PosInfinity | NegInfinity deriving (Show,Read)++data WndwOp+ = WndwRowNumber+ | WndwRank+ | WndwDenseRank+ | WndwPercentRank+ | WndwCumeDist+ | WndwNtile PrimExpr+ | WndwLag PrimExpr PrimExpr PrimExpr+ | WndwLead PrimExpr PrimExpr PrimExpr+ | WndwFirstValue PrimExpr+ | WndwLastValue PrimExpr+ | WndwNthValue PrimExpr PrimExpr+ | WndwAggregate AggrOp PrimExpr+ deriving (Show,Read)++data Partition = Partition+ { partitionBy :: [PrimExpr]+ , orderBy :: [OrderExpr]+ }+ deriving (Read, Show)
src/Opaleye/Internal/HaskellDB/Sql.hs view
@@ -31,6 +31,13 @@ , sqlOrderNulls :: SqlOrderNulls } deriving Show ++data SqlPartition = SqlPartition+ { sqlPartitionBy :: Maybe (NEL.NonEmpty SqlExpr)+ , sqlOrderBy :: Maybe (NEL.NonEmpty (SqlExpr, SqlOrder))+ }+ deriving Show+ data SqlRangeBound = Inclusive SqlExpr | Exclusive SqlExpr | PosInfinity | NegInfinity deriving Show @@ -46,6 +53,7 @@ | PostfixSqlExpr String SqlExpr | FunSqlExpr String [SqlExpr] | AggrFunSqlExpr String [SqlExpr] [(SqlExpr, SqlOrder)] SqlDistinct -- ^ Aggregate functions separate from normal functions.+ | WndwFunSqlExpr String [SqlExpr] SqlPartition | ConstSqlExpr String | CaseSqlExpr (NEL.NonEmpty (SqlExpr,SqlExpr)) SqlExpr | ListSqlExpr (NEL.NonEmpty SqlExpr)
src/Opaleye/Internal/HaskellDB/Sql/Default.hs view
@@ -50,6 +50,13 @@ PQ.NullsLast -> Sql.SqlNullsLast +toSqlPartition :: SqlGenerator -> Partition -> SqlPartition+toSqlPartition gen (Partition partition order) = SqlPartition+ { sqlPartitionBy = NEL.nonEmpty (map (sqlExpr gen) partition)+ , sqlOrderBy = NEL.nonEmpty (map (toSqlOrder gen) order)+ }++ toSqlColumn :: Attribute -> SqlColumn toSqlColumn = SqlColumn @@ -135,6 +142,9 @@ AggrDistinct -> SqlDistinct AggrAll -> SqlNotDistinct in AggrFunSqlExpr op' e' ord' distinct'+ WndwExpr op window -> let (op', e') = showWndwOp gen op+ window' = toSqlPartition gen window+ in WndwFunSqlExpr op' e' window' ConstExpr l -> ConstSqlExpr (sqlLiteral gen l) CaseExpr cs e -> let cs' = [(sqlExpr gen c, sqlExpr gen x)| (c,x) <- cs] e' = sqlExpr gen e@@ -227,6 +237,22 @@ JsonArr -> ("JSON_AGG", [sqlExpr gen arg]) AggrStringAggr sep -> ("STRING_AGG", [sqlExpr gen arg, sqlExpr gen sep]) AggrOther s -> (s, [sqlExpr gen arg])+++showWndwOp :: SqlGenerator -> WndwOp -> (String, [SqlExpr])+showWndwOp gen op = case op of+ WndwRowNumber -> ("ROW_NUMBER", [])+ WndwRank -> ("RANK", [])+ WndwDenseRank -> ("DENSE_RANK", [])+ WndwPercentRank -> ("PERCENT_RANK", [])+ WndwCumeDist -> ("CUME_DIST", [])+ WndwNtile e -> ("NTILE", [sqlExpr gen e])+ WndwLag e offset def -> ("LAG", map (sqlExpr gen) [e, offset, def])+ WndwLead e offset def -> ("LEAD", map (sqlExpr gen) [e, offset, def])+ WndwFirstValue e -> ("FIRST_VALUE", [sqlExpr gen e])+ WndwLastValue e -> ("LAST_VALUE", [sqlExpr gen e])+ WndwNthValue e n -> ("NTH_VALUE", map (sqlExpr gen) [e, n])+ WndwAggregate op' arg -> showAggrOp gen op' arg defaultSqlLiteral :: SqlGenerator -> Literal -> String
src/Opaleye/Internal/HaskellDB/Sql/Print.hs view
@@ -24,7 +24,7 @@ import Opaleye.Internal.HaskellDB.Sql (SqlColumn(..), SqlDelete(..), SqlExpr(..), SqlOrder(..), SqlInsert(..), SqlUpdate(..), SqlTable(..), SqlRangeBound(..),- OnConflict(..))+ SqlPartition(..), OnConflict(..)) import qualified Opaleye.Internal.HaskellDB.Sql as Sql import Data.List (intersperse)@@ -55,6 +55,22 @@ ppGroupAttrs :: [SqlExpr] -> Doc ppGroupAttrs = commaV (ppSqlExpr . deliteral) +ppWindowExpr :: String -> [SqlExpr] -> SqlPartition -> Doc+ppWindowExpr f es partition = text f <> parens (commaH ppSqlExpr es) <+> text "OVER" <+> parens (ppSqlPartition partition)++ppSqlPartition :: SqlPartition -> Doc+ppSqlPartition partition =+ ppPartitionBy (sqlPartitionBy partition) <+>+ ppOrderBy (maybe [] NEL.toList (sqlOrderBy partition))++ppPartitionBy :: Maybe (NEL.NonEmpty SqlExpr) -> Doc+ppPartitionBy Nothing = empty+ppPartitionBy (Just es) =+ text "PARTITION BY" <+> ppPartitionAttrs (NEL.toList es)+ where+ ppPartitionAttrs :: [SqlExpr] -> Doc+ ppPartitionAttrs = commaH (ppSqlExpr . deliteral)+ ppOrderBy :: [(SqlExpr,SqlOrder)] -> Doc ppOrderBy [] = empty ppOrderBy ord = text "ORDER BY" <+> commaV ppOrd ord@@ -177,6 +193,7 @@ ArraySqlExpr es -> text "ARRAY" <> brackets (commaH ppSqlExpr es) RangeSqlExpr t s e -> ppRange t s e AggrFunSqlExpr f es ord distinct -> text f <> parens (ppSqlDistinct distinct <+> commaH ppSqlExpr es <+> ppOrderBy ord)+ WndwFunSqlExpr f es window -> ppWindowExpr f es window CaseSqlExpr cs el -> text "CASE" <+> vcat (toList (fmap ppWhen cs)) <+> text "ELSE" <+> ppSqlExpr el <+> text "END" where ppWhen (w,t) = text "WHEN" <+> ppSqlExpr w
src/Opaleye/Internal/Optimize.hs view
@@ -53,6 +53,7 @@ \x y -> PQ.Product <$> sequenceOf (traverse._2) x <*> pure y , PQ.aggregate = fmap . PQ.Aggregate+ , PQ.window = fmap . PQ.Window , PQ.distinctOnOrderBy = \mDistinctOns -> fmap . PQ.DistinctOnOrderBy mDistinctOns , PQ.limit = fmap . PQ.Limit , PQ.join = \jt pe pq1 pq2 -> PQ.Join jt pe <$> sequence pq1 <*> sequence pq2
src/Opaleye/Internal/PrimQuery.hs view
@@ -133,6 +133,7 @@ HPQ.AggrDistinct), HPQ.Symbol)) (PrimQuery' a)+ | Window (Bindings (HPQ.WndwOp, HPQ.Partition)) (PrimQuery' a) -- | Represents both @DISTINCT ON@ and @ORDER BY@ -- clauses. In order to represent valid SQL only, -- @DISTINCT ON@ expressions are always@@ -180,6 +181,7 @@ HPQ.Symbol) -> p -> p'+ , window :: Bindings (HPQ.WndwOp, HPQ.Partition) -> p -> p' , distinctOnOrderBy :: Maybe (NEL.NonEmpty HPQ.PrimExpr) -> [HPQ.OrderExpr] -> p@@ -212,6 +214,7 @@ , baseTable = BaseTable , product = Product , aggregate = Aggregate+ , window = Window , distinctOnOrderBy = DistinctOnOrderBy , limit = Limit , join = Join@@ -236,6 +239,7 @@ , baseTable = \ti bs -> g (baseTable f ti bs) , product = \ps conds -> g (product f ((fmap . fmap) self ps) conds) , aggregate = \b p -> g (aggregate f b (self p))+ , window = \b p -> g (window f b (self p)) , distinctOnOrderBy = \m os p -> g (distinctOnOrderBy f m os (self p)) , limit = \l p -> g (limit f l (self p)) , join = \j pe lp lp' -> g (join f j pe (fmap self lp) (fmap self lp'))@@ -258,6 +262,7 @@ BaseTable ti syms -> baseTable f ti syms Product qs pes -> product f qs pes Aggregate aggrs q -> aggregate f aggrs q+ Window wndws q -> window f wndws q DistinctOnOrderBy dxs oxs q -> distinctOnOrderBy f dxs oxs q Limit op q -> limit f op q Join j cond q1 q2 -> join f j cond q1 q2
src/Opaleye/Internal/Sql.hs view
@@ -111,6 +111,7 @@ , PQ.baseTable = baseTable , PQ.product = product , PQ.aggregate = aggregate+ , PQ.window = window , PQ.distinctOnOrderBy = distinctOnOrderBy , PQ.limit = limit_ , PQ.join = join@@ -204,6 +205,12 @@ aggrExpr :: Maybe (HPQ.AggrOp, [HPQ.OrderExpr], HPQ.AggrDistinct) -> HPQ.PrimExpr -> HPQ.PrimExpr aggrExpr = maybe id (\(op, ord, distinct) e -> HPQ.AggrExpr distinct op e ord)++window :: PQ.Bindings (HPQ.WndwOp, HPQ.Partition) -> Select -> Select+window wndws' s = SelectFrom $ newSelect+ { attrs = SelectAttrsStar (ensureColumns (map (sqlBinding . fmap (uncurry HPQ.WndwExpr)) wndws'))+ , tables = oneTable s+ } distinctOnOrderBy :: Maybe (NEL.NonEmpty HPQ.PrimExpr) -> [HPQ.OrderExpr] -> Select -> Select distinctOnOrderBy distinctExprs orderExprs s = SelectFrom $ newSelect
+ src/Opaleye/Internal/Window.hs view
@@ -0,0 +1,182 @@+-- 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+ Nothing -> pure expr+ Just (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
+ src/Opaleye/Window.hs view
@@ -0,0 +1,114 @@+-- | Support for [PostgreSQL window+-- functions](https://www.postgresql.org/docs/current/tutorial-window.html)++module Opaleye.Window+ (+ -- * Run window functions on a @Select@+ W.runWindows++ -- * Create @Windows@+ , W.Windows+ , W.over++ -- * Create a @Window@+ , W.Window+ , W.partitionBy++ -- * Create a @WindowFunction@+ , W.WindowFunction++ -- * Window functions++ -- | You might like to also refer to [the Postgres+ -- documentation page that describes its window+ -- functions](https://www.postgresql.org/docs/devel/functions-window.html).++ , W.noWindowFunction+ , W.aggregatorWindowFunction+ , rowNumber+ , rank+ , denseRank+ , percentRank+ , cumeDist+ , ntile+ , lag+ , lead+ , firstValue+ , lastValue+ , nthValue+ ) where++import qualified Opaleye.Internal.Column as IC+import qualified Opaleye.Internal.HaskellDB.PrimQuery as HPQ+import qualified Opaleye.Internal.Window as W++import qualified Opaleye.Field as F+import qualified Opaleye.SqlTypes as T++-- | [@row_number()@](https://www.postgresql.org/docs/current/functions-window.html)+rowNumber :: W.WindowFunction a (F.Field T.SqlInt8)+rowNumber = W.makeWndwAny HPQ.WndwRowNumber+++-- | [@rank()@](https://www.postgresql.org/docs/current/functions-window.html)+rank :: W.WindowFunction a (F.Field T.SqlInt8)+rank = W.makeWndwAny HPQ.WndwRank+++-- | [@dense_rank()@](https://www.postgresql.org/docs/current/functions-window.html)+denseRank :: W.WindowFunction a (F.Field T.SqlInt8)+denseRank = W.makeWndwAny HPQ.WndwDenseRank+++-- | [@percent_rank()@](https://www.postgresql.org/docs/current/functions-window.html)+percentRank :: W.WindowFunction a (F.Field T.SqlFloat8)+percentRank = W.makeWndwAny HPQ.WndwPercentRank+++-- | [@cume_dist()@](https://www.postgresql.org/docs/current/functions-window.html)+cumeDist :: W.WindowFunction a (F.Field T.SqlFloat8)+cumeDist = W.makeWndwAny HPQ.WndwCumeDist+++-- | [@ntile(num_buckets)@](https://www.postgresql.org/docs/current/functions-window.html)+ntile :: F.Field T.SqlInt4+ -- ^ num_buckets+ -> W.WindowFunction a (F.Field T.SqlInt4)+ntile (IC.Column buckets) = W.makeWndwAny $ HPQ.WndwNtile buckets+++-- | [@lag(value, offset, default)@](https://www.postgresql.org/docs/current/functions-window.html)+lag :: F.Field T.SqlInt4+ -- ^ offset+ -> F.Field_ n a+ -- ^ default+ -> W.WindowFunction (F.Field_ n a) (F.Field_ n a)+lag (IC.Column offset) (IC.Column def) =+ W.makeWndwField $ \a -> HPQ.WndwLag a offset def+++-- | [@lead(value, offset, default)@](https://www.postgresql.org/docs/current/functions-window.html)+lead :: F.Field T.SqlInt4+ -- ^ offset+ -> F.Field_ n a+ -- ^ default+ -> W.WindowFunction (F.Field_ n a) (F.Field_ n a)+lead (IC.Column offset) (IC.Column def) =+ W.makeWndwField $ \a -> HPQ.WndwLead a offset def+++-- | [@first_value(value)@](https://www.postgresql.org/docs/current/functions-window.html)+firstValue :: W.WindowFunction (F.Field_ n a) (F.Field_ n a)+firstValue = W.makeWndwField HPQ.WndwFirstValue+++-- | [@last_value(value)@](https://www.postgresql.org/docs/current/functions-window.html)+lastValue :: W.WindowFunction (F.Field_ n a) (F.Field_ n a)+lastValue = W.makeWndwField HPQ.WndwLastValue+++-- | [@nth_value(value, n)@](https://www.postgresql.org/docs/current/functions-window.html)+nthValue :: F.Field T.SqlInt4+ -- ^ n+ -> W.WindowFunction (F.Field_ n a) (F.FieldNullable a)+nthValue (IC.Column n) = W.makeWndwField $ \a -> HPQ.WndwNthValue a n