mirror of
https://github.com/flipstone/orville.git
synced 2024-10-03 23:27:49 +03:00
Merge branch 'telser/windowing-expr-and-functions'
This commit is contained in:
commit
5f9291aeb8
@ -130,11 +130,16 @@ library
|
||||
Orville.PostgreSQL.Expr.Internal.Name.SequenceName
|
||||
Orville.PostgreSQL.Expr.Internal.Name.TableName
|
||||
Orville.PostgreSQL.Expr.Internal.Name.TriggerName
|
||||
Orville.PostgreSQL.Expr.Internal.Name.WindowName
|
||||
Orville.PostgreSQL.Expr.Join
|
||||
Orville.PostgreSQL.Expr.OrReplace
|
||||
Orville.PostgreSQL.Expr.RowLocking
|
||||
Orville.PostgreSQL.Expr.Trigger
|
||||
Orville.PostgreSQL.Expr.Vacuum
|
||||
Orville.PostgreSQL.Expr.Window
|
||||
Orville.PostgreSQL.Expr.Window.WindowClause
|
||||
Orville.PostgreSQL.Expr.Window.WindowDefinitionExpr
|
||||
Orville.PostgreSQL.Expr.Window.WindowFunction
|
||||
Orville.PostgreSQL.Internal.Bracket
|
||||
Orville.PostgreSQL.Internal.Extra.NonEmpty
|
||||
Orville.PostgreSQL.Internal.FieldName
|
||||
@ -217,6 +222,7 @@ test-suite spec
|
||||
Test.Expr.Trigger
|
||||
Test.Expr.Vacuum
|
||||
Test.Expr.Where
|
||||
Test.Expr.Window
|
||||
Test.FieldDefinition
|
||||
Test.MarshallError
|
||||
Test.PgAssert
|
||||
|
@ -16,6 +16,7 @@ module Orville.PostgreSQL.Execution.SelectOptions
|
||||
, selectLimitExpr
|
||||
, selectOffsetExpr
|
||||
, selectRowLockingClause
|
||||
, selectWindowClause
|
||||
, distinct
|
||||
, where_
|
||||
, orderBy
|
||||
@ -23,6 +24,7 @@ module Orville.PostgreSQL.Execution.SelectOptions
|
||||
, offset
|
||||
, groupBy
|
||||
, forRowLock
|
||||
, window
|
||||
, selectOptionsQueryExpr
|
||||
)
|
||||
where
|
||||
@ -47,6 +49,7 @@ data SelectOptions = SelectOptions
|
||||
, i_offsetExpr :: First Expr.OffsetExpr
|
||||
, i_groupByExpr :: Maybe Expr.GroupByExpr
|
||||
, i_rowLockingClause :: First Expr.RowLockingClause
|
||||
, i_windowExpr :: Maybe Expr.NamedWindowDefinitionExpr
|
||||
}
|
||||
|
||||
-- | @since 1.0.0.0
|
||||
@ -72,6 +75,7 @@ emptySelectOptions =
|
||||
, i_offsetExpr = mempty
|
||||
, i_groupByExpr = mempty
|
||||
, i_rowLockingClause = mempty
|
||||
, i_windowExpr = Nothing
|
||||
}
|
||||
|
||||
{- |
|
||||
@ -91,6 +95,7 @@ appendSelectOptions left right =
|
||||
(i_offsetExpr left <> i_offsetExpr right)
|
||||
(i_groupByExpr left <> i_groupByExpr right)
|
||||
(i_rowLockingClause left <> i_rowLockingClause right)
|
||||
(i_windowExpr left <> i_windowExpr right)
|
||||
|
||||
unionMaybeWith :: (a -> a -> a) -> Maybe a -> Maybe a -> Maybe a
|
||||
unionMaybeWith f mbLeft mbRight =
|
||||
@ -185,6 +190,15 @@ selectRowLockingClause :: SelectOptions -> Maybe Expr.RowLockingClause
|
||||
selectRowLockingClause =
|
||||
getFirst . i_rowLockingClause
|
||||
|
||||
{- |
|
||||
Builds an 'Expr.WindowClause' that will apply the windowing rules specified in the 'SelectOptions' (if any).
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
selectWindowClause :: SelectOptions -> Maybe Expr.WindowClause
|
||||
selectWindowClause =
|
||||
fmap Expr.windowClause . i_windowExpr
|
||||
|
||||
{- |
|
||||
Constructs a 'SelectOptions' with just the given 'Expr.BooleanExpr'.
|
||||
|
||||
@ -251,6 +265,17 @@ forRowLock rowLockingClause =
|
||||
{ i_rowLockingClause = First $ Just rowLockingClause
|
||||
}
|
||||
|
||||
{- |
|
||||
Constructs a 'SelectOptions' with just the given 'Expr.NamedWindowDefinitionExpr'.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
window :: Expr.NamedWindowDefinitionExpr -> SelectOptions
|
||||
window namedWindow =
|
||||
emptySelectOptions
|
||||
{ i_windowExpr = Just namedWindow
|
||||
}
|
||||
|
||||
{- |
|
||||
Builds a 'Expr.QueryExpr' that will use the specified 'Expr.SelectList' when
|
||||
building the @SELECT@ statement to execute. It is up to the caller to make
|
||||
@ -286,4 +311,5 @@ selectOptionsQueryExpr selectList tableReferenceList selectOptions =
|
||||
(selectLimitExpr selectOptions)
|
||||
(selectOffsetExpr selectOptions)
|
||||
(selectRowLockingClause selectOptions)
|
||||
(selectWindowClause selectOptions)
|
||||
)
|
||||
|
@ -69,6 +69,7 @@ module Orville.PostgreSQL.Expr
|
||||
, module Orville.PostgreSQL.Expr.Function
|
||||
, module Orville.PostgreSQL.Expr.OrReplace
|
||||
, module Orville.PostgreSQL.Expr.ConditionalExpr
|
||||
, module Orville.PostgreSQL.Expr.Window
|
||||
, module Orville.PostgreSQL.Expr.Vacuum
|
||||
, module Orville.PostgreSQL.Expr.Extension
|
||||
, module Orville.PostgreSQL.Expr.RowLocking
|
||||
@ -115,3 +116,4 @@ import Orville.PostgreSQL.Expr.Update
|
||||
import Orville.PostgreSQL.Expr.Vacuum
|
||||
import Orville.PostgreSQL.Expr.ValueExpression
|
||||
import Orville.PostgreSQL.Expr.WhereClause
|
||||
import Orville.PostgreSQL.Expr.Window
|
||||
|
@ -0,0 +1,48 @@
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
|
||||
{- |
|
||||
Copyright : Flipstone Technology Partners 2024
|
||||
License : MIT
|
||||
Stability : Stable
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
module Orville.PostgreSQL.Expr.Internal.Name.WindowName
|
||||
( WindowName
|
||||
, windowName
|
||||
)
|
||||
where
|
||||
|
||||
import Orville.PostgreSQL.Expr.Internal.Name.Identifier (Identifier, IdentifierExpression, identifier)
|
||||
import qualified Orville.PostgreSQL.Raw.RawSql as RawSql
|
||||
|
||||
{- |
|
||||
Type to represent a SQL window name. 'WindowName' values constructed
|
||||
via the 'windowName' window will be properly escaped as part of the
|
||||
generated SQL. E.G.
|
||||
|
||||
> "some_window_name"
|
||||
|
||||
'WindowName' provides a 'RawSql.SqlExpression' instance. See
|
||||
'RawSql.unsafeSqlExpression' for how to construct a value with your own custom
|
||||
SQL.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
newtype WindowName
|
||||
= WindowName Identifier
|
||||
deriving
|
||||
( -- | @since 1.1.0.0
|
||||
RawSql.SqlExpression
|
||||
, -- | @since 1.1.0.0
|
||||
IdentifierExpression
|
||||
)
|
||||
|
||||
{- |
|
||||
Construct a 'WindowName' from a 'String' with proper escaping as part of the generated SQL.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
windowName :: String -> WindowName
|
||||
windowName =
|
||||
WindowName . identifier
|
@ -26,3 +26,4 @@ import Orville.PostgreSQL.Expr.Internal.Name.SchemaName as Export
|
||||
import Orville.PostgreSQL.Expr.Internal.Name.SequenceName as Export
|
||||
import Orville.PostgreSQL.Expr.Internal.Name.TableName as Export
|
||||
import Orville.PostgreSQL.Expr.Internal.Name.TriggerName as Export
|
||||
import Orville.PostgreSQL.Expr.Internal.Name.WindowName as Export
|
||||
|
@ -46,6 +46,7 @@ import Orville.PostgreSQL.Expr.RowLocking (RowLockingClause)
|
||||
import Orville.PostgreSQL.Expr.Select (SelectClause)
|
||||
import Orville.PostgreSQL.Expr.ValueExpression (ValueExpression, columnReference)
|
||||
import Orville.PostgreSQL.Expr.WhereClause (BooleanExpr, InValuePredicate, WhereClause, inPredicate, notInPredicate)
|
||||
import Orville.PostgreSQL.Expr.Window (WindowClause)
|
||||
import qualified Orville.PostgreSQL.Raw.RawSql as RawSql
|
||||
|
||||
-- This is a rough model of "query specification" see https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#_7_16_query_specification for more detail than you probably want
|
||||
@ -339,6 +340,8 @@ tableExpr ::
|
||||
Maybe OffsetExpr ->
|
||||
-- | An optional locking clause to apply to the result set.
|
||||
Maybe RowLockingClause ->
|
||||
-- | An optional @WINDOW@ clause to apply to the result set.
|
||||
Maybe WindowClause ->
|
||||
TableExpr
|
||||
tableExpr
|
||||
tableReferenceList
|
||||
@ -347,13 +350,15 @@ tableExpr
|
||||
maybeOrderByClause
|
||||
maybeLimitExpr
|
||||
maybeOffsetExpr
|
||||
maybeRowLockingClause =
|
||||
maybeRowLockingClause
|
||||
maybeWindowClause =
|
||||
TableExpr
|
||||
. RawSql.intercalate RawSql.space
|
||||
$ RawSql.toRawSql tableReferenceList
|
||||
: catMaybes
|
||||
[ RawSql.toRawSql <$> maybeWhereClause
|
||||
, RawSql.toRawSql <$> maybeGroupByClause
|
||||
, RawSql.toRawSql <$> maybeWindowClause
|
||||
, RawSql.toRawSql <$> maybeOrderByClause
|
||||
, RawSql.toRawSql <$> maybeLimitExpr
|
||||
, RawSql.toRawSql <$> maybeOffsetExpr
|
||||
|
18
orville-postgresql/src/Orville/PostgreSQL/Expr/Window.hs
Normal file
18
orville-postgresql/src/Orville/PostgreSQL/Expr/Window.hs
Normal file
@ -0,0 +1,18 @@
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# OPTIONS_GHC -Wno-missing-import-lists #-}
|
||||
|
||||
{- |
|
||||
Copyright : Flipstone Technology Partners 2024
|
||||
License : MIT
|
||||
Stability : Stable
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
module Orville.PostgreSQL.Expr.Window
|
||||
( module Export
|
||||
)
|
||||
where
|
||||
|
||||
import Orville.PostgreSQL.Expr.Window.WindowClause as Export
|
||||
import Orville.PostgreSQL.Expr.Window.WindowDefinitionExpr as Export
|
||||
import Orville.PostgreSQL.Expr.Window.WindowFunction as Export
|
@ -0,0 +1,92 @@
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
|
||||
{- |
|
||||
Copyright : Flipstone Technology Partners 2024
|
||||
License : MIT
|
||||
Stability : Stable
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
module Orville.PostgreSQL.Expr.Window.WindowClause
|
||||
( WindowClause
|
||||
, windowClause
|
||||
, NamedWindowDefinitionExpr
|
||||
, namedWindowDefinition
|
||||
, appendNamedWindowDefinitionExpr
|
||||
)
|
||||
where
|
||||
|
||||
import Orville.PostgreSQL.Expr.Name (WindowName)
|
||||
import Orville.PostgreSQL.Expr.Window.WindowDefinitionExpr (WindowDefinitionExpr)
|
||||
import qualified Orville.PostgreSQL.Raw.RawSql as RawSql
|
||||
|
||||
{- | Type to represent a named SQL window definition. This should only be used in a @WINDOW@ clause.
|
||||
|
||||
'NamedWindowDefinitionExpr' provides a 'RawSql.SqlExpression' instance.
|
||||
See 'RawSql.unsafeSqlExpression' for how to construct a value with your own custom SQL.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
newtype NamedWindowDefinitionExpr = NamedWindowDefinitionExpr RawSql.RawSql
|
||||
deriving
|
||||
( -- | @since 1.1.0.0
|
||||
RawSql.SqlExpression
|
||||
)
|
||||
|
||||
{- |
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
instance Semigroup NamedWindowDefinitionExpr where
|
||||
(<>) = appendNamedWindowDefinitionExpr
|
||||
|
||||
{- | Combines two 'NamedWindowDefinitionExpr's with a comma between them.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
appendNamedWindowDefinitionExpr :: NamedWindowDefinitionExpr -> NamedWindowDefinitionExpr -> NamedWindowDefinitionExpr
|
||||
appendNamedWindowDefinitionExpr (NamedWindowDefinitionExpr a) (NamedWindowDefinitionExpr b) =
|
||||
NamedWindowDefinitionExpr (a <> RawSql.commaSpace <> b)
|
||||
|
||||
{- |
|
||||
Builds a 'NamedWindowDefinitionExpr' with the given name and 'WindowDefinitionExpr'.
|
||||
1
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
namedWindowDefinition :: WindowName -> WindowDefinitionExpr -> NamedWindowDefinitionExpr
|
||||
namedWindowDefinition windowName expr =
|
||||
NamedWindowDefinitionExpr $
|
||||
RawSql.toRawSql windowName
|
||||
<> RawSql.space
|
||||
<> RawSql.fromString "AS"
|
||||
<> RawSql.space
|
||||
<> RawSql.parenthesized (RawSql.toRawSql expr)
|
||||
|
||||
{- |
|
||||
Type to represent a SQL window clause. E.G.
|
||||
|
||||
> WINDOW foo , bar
|
||||
|
||||
'WindowClause' provides a 'RawSql.SqlExpression' instance. See
|
||||
'RawSql.unsafeSqlExpression' for how to construct a value with your own custom
|
||||
SQL.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
newtype WindowClause
|
||||
= WindowClause RawSql.RawSql
|
||||
deriving
|
||||
( -- | @since 1.1.0.0
|
||||
RawSql.SqlExpression
|
||||
)
|
||||
|
||||
{- |
|
||||
Builds a full 'WindowClause' with the given windowing described in the 'NamedWindowDefinitionExpr'
|
||||
1
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
windowClause :: NamedWindowDefinitionExpr -> WindowClause
|
||||
windowClause namedWindowDefinitionExpr =
|
||||
WindowClause $
|
||||
RawSql.fromString "WINDOW"
|
||||
<> RawSql.space
|
||||
<> RawSql.toRawSql namedWindowDefinitionExpr
|
@ -0,0 +1,316 @@
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
|
||||
{- |
|
||||
Copyright : Flipstone Technology Partners 2024
|
||||
License : MIT
|
||||
Stability : Stable
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
module Orville.PostgreSQL.Expr.Window.WindowDefinitionExpr
|
||||
( WindowDefinitionExpr
|
||||
, windowDefinition
|
||||
, PartitionByExpr
|
||||
, partitionBy
|
||||
, FrameClause
|
||||
, frameClause
|
||||
, FrameModeExpr
|
||||
, rangeFrameMode
|
||||
, rowsFrameMode
|
||||
, groupsFrameMode
|
||||
, FrameStartExpr
|
||||
, unboundedPrecedingFrameStart
|
||||
, offsetPrecedingFrameStart
|
||||
, currentRowFrameStart
|
||||
, offsetFollowingFrameStart
|
||||
, FrameEndExpr
|
||||
, offsetPrecedingFrameEnd
|
||||
, currentRowFrameEnd
|
||||
, offsetFollowingFrameEnd
|
||||
, unboundedFollowingFrameEnd
|
||||
, FrameExclusionExpr
|
||||
, currentRowFrameExclusion
|
||||
, groupFrameExclusion
|
||||
, tiesFrameExclusion
|
||||
, noOthersFrameExclusion
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Data.List.NonEmpty as NEL
|
||||
import Data.Maybe (catMaybes)
|
||||
|
||||
import Orville.PostgreSQL.Expr.Name (WindowName)
|
||||
import qualified Orville.PostgreSQL.Expr.OrderBy as OrderBy
|
||||
import qualified Orville.PostgreSQL.Expr.ValueExpression as ValueExpression
|
||||
import qualified Orville.PostgreSQL.Raw.RawSql as RawSql
|
||||
|
||||
{- | Type to represent a SQL window definition. This can be used in either a @WINDOW@ clause or in the
|
||||
portion of a window function call after the @OVER@.
|
||||
|
||||
'WindowDefinitionExpr' provides a 'RawSql.SqlExpression' instance.
|
||||
See 'RawSql.unsafeSqlExpression' for how to construct a value with your own custom SQL.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
newtype WindowDefinitionExpr = WindowDefinitionExpr RawSql.RawSql
|
||||
deriving
|
||||
( -- | @since 1.1.0.0
|
||||
RawSql.SqlExpression
|
||||
)
|
||||
|
||||
{- | Builds a 'WindowDefinitionExpr'. Note that it is up to the caller to ensure the options make
|
||||
sense togther, including particular rules below.
|
||||
|
||||
N.B. If the 'WindowName' is used (so we are "copying" a previous definition), then a few rules apply:
|
||||
* The 'ParititionByExpr' and 'OrderByClause' will be copied if any
|
||||
* If either 'ParitionByExpr' and 'OrderByClause' exist then a 'PartitionByExpr' may not be specified.
|
||||
* The 'OrderByClause' can _only_ be specified if the window copied does not have one.
|
||||
* The copied window must not specify a 'FrameClause'.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
windowDefinition ::
|
||||
-- | An, optional, already existing named window that must be named in a prior @WINDOW@ entry.
|
||||
Maybe WindowName ->
|
||||
Maybe PartitionByExpr ->
|
||||
Maybe OrderBy.OrderByClause ->
|
||||
Maybe FrameClause ->
|
||||
WindowDefinitionExpr
|
||||
windowDefinition mbWindowName mbPartitionBy mbOrderBy mbFrame =
|
||||
WindowDefinitionExpr
|
||||
. RawSql.intercalate RawSql.space
|
||||
$ catMaybes
|
||||
[ fmap RawSql.toRawSql mbWindowName
|
||||
, fmap RawSql.toRawSql mbPartitionBy
|
||||
, fmap RawSql.toRawSql mbOrderBy
|
||||
, fmap RawSql.toRawSql mbFrame
|
||||
]
|
||||
|
||||
{- |
|
||||
Type to represent the @PARTITION BY expression [, ...]@ portion of a SQL window definition.
|
||||
|
||||
'PartitionByExpr' provides a 'RawSql.SqlExpression' instance.
|
||||
See 'RawSql.unsafeSqlExpression' for how to construct a value with your own custom SQL.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
newtype PartitionByExpr = PartitionByExpr RawSql.RawSql
|
||||
deriving
|
||||
( -- | @since 1.1.0.0
|
||||
RawSql.SqlExpression
|
||||
)
|
||||
|
||||
{- |
|
||||
Builds a 'PartitionByExpr'.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
partitionBy :: NEL.NonEmpty ValueExpression.ValueExpression -> PartitionByExpr
|
||||
partitionBy exprs =
|
||||
PartitionByExpr $
|
||||
RawSql.fromString "PARTITION BY "
|
||||
<> RawSql.intercalate RawSql.commaSpace exprs
|
||||
|
||||
{- |
|
||||
Type to represent the framing clause of a window definition.
|
||||
|
||||
'FrameClause' provides a 'RawSql.SqlExpression' instance.
|
||||
See 'RawSql.unsafeSqlExpression' for how to construct a value with your own custom SQL.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
newtype FrameClause = FrameClause RawSql.RawSql
|
||||
deriving
|
||||
( -- | @since 1.1.0.0
|
||||
RawSql.SqlExpression
|
||||
)
|
||||
|
||||
{- | Builds a 'FrameClause' from the given pieces.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
frameClause :: Maybe FrameModeExpr -> FrameStartExpr -> Maybe FrameEndExpr -> Maybe FrameExclusionExpr -> FrameClause
|
||||
frameClause mbFrameMode frameStart mbFrameEnd mbExclusion =
|
||||
let
|
||||
mode =
|
||||
maybe mempty RawSql.toRawSql mbFrameMode
|
||||
exclusion =
|
||||
maybe mempty RawSql.toRawSql mbExclusion
|
||||
in
|
||||
FrameClause $
|
||||
mode
|
||||
<> RawSql.space
|
||||
<> case mbFrameEnd of
|
||||
Nothing -> RawSql.toRawSql frameStart
|
||||
Just frameEnd ->
|
||||
RawSql.fromString "BETWEEN "
|
||||
<> RawSql.toRawSql frameStart
|
||||
<> RawSql.fromString " AND "
|
||||
<> RawSql.toRawSql frameEnd
|
||||
<> RawSql.space
|
||||
<> exclusion
|
||||
|
||||
{- |
|
||||
Type to represent the mode of the framing in a window definition.
|
||||
|
||||
'FrameModeExpr' provides a 'RawSql.SqlExpression' instance.
|
||||
See 'RawSql.unsafeSqlExpression' for how to construct a value with your own custom SQL.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
newtype FrameModeExpr = FrameModeExpr RawSql.RawSql
|
||||
deriving
|
||||
( -- | @since 1.1.0.0
|
||||
RawSql.SqlExpression
|
||||
)
|
||||
|
||||
{- | Value for the frame mode of @RANGE@
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
rangeFrameMode :: FrameModeExpr
|
||||
rangeFrameMode = FrameModeExpr $ RawSql.fromString "RANGE"
|
||||
|
||||
{- | Value for the frame mode of @ROWS@
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
rowsFrameMode :: FrameModeExpr
|
||||
rowsFrameMode = FrameModeExpr $ RawSql.fromString "ROWS"
|
||||
|
||||
{- | Value for the frame mode of @GROUPS@
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
groupsFrameMode :: FrameModeExpr
|
||||
groupsFrameMode = FrameModeExpr $ RawSql.fromString "GROUPS"
|
||||
|
||||
{- |
|
||||
Type to represent the beginning of the framing in a window definition.
|
||||
|
||||
'FrameStartExpr' provides a 'RawSql.SqlExpression' instance.
|
||||
See 'RawSql.unsafeSqlExpression' for how to construct a value with your own custom SQL.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
newtype FrameStartExpr = FrameStartExpr RawSql.RawSql
|
||||
deriving
|
||||
( -- | @since 1.1.0.0
|
||||
RawSql.SqlExpression
|
||||
)
|
||||
|
||||
{- | Value for a frame start of @UNBOUNDED PRECEDING@
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
unboundedPrecedingFrameStart :: FrameStartExpr
|
||||
unboundedPrecedingFrameStart = FrameStartExpr $ RawSql.fromString "UNBOUNDED PRECEDING"
|
||||
|
||||
{- | Value for a frame start of @expression PRECEDING@. Note that it is up to the caller to ensure
|
||||
that the 'ValueExpression' is valid in this context.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
offsetPrecedingFrameStart :: ValueExpression.ValueExpression -> FrameStartExpr
|
||||
offsetPrecedingFrameStart val = FrameStartExpr $ RawSql.toRawSql val <> RawSql.fromString " PRECEDING"
|
||||
|
||||
{- | Value for a frame start of @CURRENT ROW@
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
currentRowFrameStart :: FrameStartExpr
|
||||
currentRowFrameStart = FrameStartExpr $ RawSql.fromString "CURRENT ROW"
|
||||
|
||||
{- | Value for a frame start of @expression FOLLOWING@. Note that it is up to the caller to ensure
|
||||
that the 'ValueExpression' is valid in this context.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
offsetFollowingFrameStart :: ValueExpression.ValueExpression -> FrameStartExpr
|
||||
offsetFollowingFrameStart val = FrameStartExpr $ RawSql.toRawSql val <> RawSql.fromString " FOLLOWING"
|
||||
|
||||
{- |
|
||||
Type to represent the ending of the framing in a window definition.
|
||||
|
||||
'FrameEndExpr' provides a 'RawSql.SqlExpression' instance.
|
||||
See 'RawSql.unsafeSqlExpression' for how to construct a value with your own custom SQL.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
newtype FrameEndExpr = FrameEndExpr RawSql.RawSql
|
||||
deriving
|
||||
( -- | @since 1.1.0.0
|
||||
RawSql.SqlExpression
|
||||
)
|
||||
|
||||
{- | Value for a frame end of @expression PRECEDING@. Note that it is up to the caller to ensure
|
||||
that the 'ValueExpression' is valid in this context.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
offsetPrecedingFrameEnd :: ValueExpression.ValueExpression -> FrameEndExpr
|
||||
offsetPrecedingFrameEnd val = FrameEndExpr $ RawSql.toRawSql val <> RawSql.fromString " PRECEDING"
|
||||
|
||||
{- | Value for a frame end of @CURRENT ROW@
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
currentRowFrameEnd :: FrameEndExpr
|
||||
currentRowFrameEnd = FrameEndExpr $ RawSql.fromString "CURRENT ROW"
|
||||
|
||||
{- | Value for a frame end of @expression FOLLOWING@. Note that it is up to the caller to ensure
|
||||
that the 'ValueExpression' is valid in this context.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
offsetFollowingFrameEnd :: ValueExpression.ValueExpression -> FrameEndExpr
|
||||
offsetFollowingFrameEnd val = FrameEndExpr $ RawSql.toRawSql val <> RawSql.fromString " FOLLOWING"
|
||||
|
||||
{- | Value for a frame end of @UNBOUNDED FOLLOWING@
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
unboundedFollowingFrameEnd :: FrameEndExpr
|
||||
unboundedFollowingFrameEnd = FrameEndExpr $ RawSql.fromString "UNBOUNDED FOLLOWING"
|
||||
|
||||
{- |
|
||||
Type to represent the exclusion of results in the framing in a window definition.
|
||||
|
||||
'FrameExclusionExpr' provides a 'RawSql.SqlExpression' instance.
|
||||
See 'RawSql.unsafeSqlExpression' for how to construct a value with your own custom SQL.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
newtype FrameExclusionExpr = FrameExclusionExpr RawSql.RawSql
|
||||
deriving
|
||||
( -- | @since 1.1.0.0
|
||||
RawSql.SqlExpression
|
||||
)
|
||||
|
||||
{- | Value for a frame exclusion of @EXCLUDE CURRENT ROW@
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
currentRowFrameExclusion :: FrameExclusionExpr
|
||||
currentRowFrameExclusion = FrameExclusionExpr $ RawSql.fromString "EXCLUDE CURRENT ROW"
|
||||
|
||||
{- | Value for a frame exclusion of @EXCLUDE GROUP@
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
groupFrameExclusion :: FrameExclusionExpr
|
||||
groupFrameExclusion = FrameExclusionExpr $ RawSql.fromString "EXCLUDE GROUP"
|
||||
|
||||
{- | Value for a frame exclusion of @EXCLUDE TIES@
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
tiesFrameExclusion :: FrameExclusionExpr
|
||||
tiesFrameExclusion = FrameExclusionExpr $ RawSql.fromString "EXCLUDE TIES"
|
||||
|
||||
{- | Value for a frame exclusion of @EXCLUDE NO OTHERS@
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
noOthersFrameExclusion :: FrameExclusionExpr
|
||||
noOthersFrameExclusion = FrameExclusionExpr $ RawSql.fromString "EXCLUDE NO OTHERS"
|
@ -0,0 +1,79 @@
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
|
||||
{- |
|
||||
Copyright : Flipstone Technology Partners 2024
|
||||
License : MIT
|
||||
Stability : Stable
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
module Orville.PostgreSQL.Expr.Window.WindowFunction
|
||||
( rowNumber
|
||||
, rank
|
||||
, denseRank
|
||||
, percentRank
|
||||
, windowFunction
|
||||
)
|
||||
where
|
||||
|
||||
import Orville.PostgreSQL.Expr.Name (FunctionName, functionName)
|
||||
import qualified Orville.PostgreSQL.Expr.ValueExpression as ValueExpression
|
||||
import qualified Orville.PostgreSQL.Expr.WhereClause as WhereClause
|
||||
import Orville.PostgreSQL.Expr.Window.WindowDefinitionExpr (WindowDefinitionExpr)
|
||||
import qualified Orville.PostgreSQL.Raw.RawSql as RawSql
|
||||
|
||||
{- |
|
||||
The @row_number@ window function
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
rowNumber :: Maybe WhereClause.WhereClause -> WindowDefinitionExpr -> ValueExpression.ValueExpression
|
||||
rowNumber = windowFunction (functionName "row_number") []
|
||||
|
||||
{- |
|
||||
The @rank@ window function
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
rank :: Maybe WhereClause.WhereClause -> WindowDefinitionExpr -> ValueExpression.ValueExpression
|
||||
rank = windowFunction (functionName "rank") []
|
||||
|
||||
{- |
|
||||
The @dense_rank@ window function
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
denseRank :: Maybe WhereClause.WhereClause -> WindowDefinitionExpr -> ValueExpression.ValueExpression
|
||||
denseRank = windowFunction (functionName "dense_rank") []
|
||||
|
||||
{- |
|
||||
The @percent_rank@ window function
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
percentRank :: Maybe WhereClause.WhereClause -> WindowDefinitionExpr -> ValueExpression.ValueExpression
|
||||
percentRank = windowFunction (functionName "percent_rank") []
|
||||
|
||||
{- | Build a 'ValueExpression' that represents a window function call. It is up to the caller to
|
||||
ensure this makes sense as windowing functions are not allowed in all places.
|
||||
|
||||
@since 1.1.0.0
|
||||
-}
|
||||
windowFunction :: FunctionName -> [ValueExpression.ValueExpression] -> Maybe WhereClause.WhereClause -> WindowDefinitionExpr -> ValueExpression.ValueExpression
|
||||
windowFunction function parameters mbWhereClause windowDef =
|
||||
let
|
||||
filtering =
|
||||
case mbWhereClause of
|
||||
Nothing -> mempty
|
||||
Just whereClause ->
|
||||
RawSql.fromString "FILTER"
|
||||
<> RawSql.parenthesized (RawSql.toRawSql whereClause)
|
||||
in
|
||||
RawSql.unsafeFromRawSql $
|
||||
RawSql.toRawSql (ValueExpression.functionCall function parameters)
|
||||
<> RawSql.space
|
||||
<> filtering
|
||||
<> RawSql.space
|
||||
<> RawSql.fromString "OVER"
|
||||
<> RawSql.space
|
||||
<> RawSql.toRawSql windowDef
|
@ -32,6 +32,7 @@ import qualified Test.Expr.Time as ExprTime
|
||||
import qualified Test.Expr.Trigger as ExprTrigger
|
||||
import qualified Test.Expr.Vacuum as ExprVacuum
|
||||
import qualified Test.Expr.Where as ExprWhere
|
||||
import qualified Test.Expr.Window as ExprWindow
|
||||
import qualified Test.FieldDefinition as FieldDefinition
|
||||
import qualified Test.MarshallError as MarshallError
|
||||
import qualified Test.PgCatalog as PgCatalog
|
||||
@ -76,6 +77,7 @@ main = do
|
||||
, ExprJoin.joinTests
|
||||
, ExprFromItem.fromItemExprTests
|
||||
, ExprConditional.conditionalTests
|
||||
, ExprWindow.windowTests
|
||||
, FieldDefinition.fieldDefinitionTests pool
|
||||
, SqlMarshaller.sqlMarshallerTests
|
||||
, MarshallError.marshallErrorTests pool
|
||||
|
@ -61,7 +61,7 @@ prop_countColumn =
|
||||
(Expr.columnName "count")
|
||||
]
|
||||
)
|
||||
(Just (Expr.tableExpr (Expr.tableFromItem $ Orville.tableName Foo.table) Nothing Nothing Nothing Nothing Nothing Nothing))
|
||||
(Just (Expr.tableExpr (Expr.tableFromItem $ Orville.tableName Foo.table) Nothing Nothing Nothing Nothing Nothing Nothing Nothing))
|
||||
|
||||
marshaller =
|
||||
Orville.annotateSqlMarshallerEmptyAnnotation $
|
||||
|
@ -97,7 +97,7 @@ groupByTest testName test =
|
||||
Expr.queryExpr
|
||||
(Expr.selectClause $ Expr.selectExpr Nothing)
|
||||
(Expr.selectColumns [fooColumn, barColumn])
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem testTable) Nothing (groupByClause test) Nothing Nothing Nothing Nothing)
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem testTable) Nothing (groupByClause test) Nothing Nothing Nothing Nothing Nothing)
|
||||
|
||||
Execution.readRows result
|
||||
|
||||
|
@ -87,7 +87,7 @@ groupByOrderByTest testName test =
|
||||
Expr.queryExpr
|
||||
(Expr.selectClause $ Expr.selectExpr Nothing)
|
||||
(Expr.selectColumns [fooColumn, barColumn])
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem testTable) Nothing (groupByClause test) (orderByClause test) Nothing Nothing Nothing)
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem testTable) Nothing (groupByClause test) (orderByClause test) Nothing Nothing Nothing Nothing)
|
||||
|
||||
Execution.readRows result
|
||||
|
||||
|
@ -127,7 +127,7 @@ orderByTest testName test =
|
||||
Expr.queryExpr
|
||||
(Expr.selectClause $ Expr.selectExpr Nothing)
|
||||
(Expr.selectColumns [fooColumn, barColumn])
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem fooBarTable) Nothing Nothing (orderByClause test) Nothing Nothing Nothing)
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem fooBarTable) Nothing Nothing (orderByClause test) Nothing Nothing Nothing Nothing)
|
||||
|
||||
Execution.readRows result
|
||||
|
||||
|
@ -83,7 +83,7 @@ findAllFooBarsInTable tableName =
|
||||
Expr.queryExpr
|
||||
(Expr.selectClause $ Expr.selectExpr Nothing)
|
||||
(Expr.selectColumns [fooColumn, barColumnAliased])
|
||||
(Just $ Expr.tableExpr tableRef Nothing Nothing (Just orderByFoo) Nothing Nothing Nothing)
|
||||
(Just $ Expr.tableExpr tableRef Nothing Nothing (Just orderByFoo) Nothing Nothing Nothing Nothing)
|
||||
|
||||
encodeFooBar :: FooBar -> [(Maybe B8.ByteString, SqlValue.SqlValue)]
|
||||
encodeFooBar fooBar =
|
||||
|
@ -252,7 +252,7 @@ whereConditionTest testName test =
|
||||
Expr.queryExpr
|
||||
(Expr.selectClause $ Expr.selectExpr Nothing)
|
||||
(Expr.selectColumns [fooColumn, barColumn])
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem fooBarTable) (whereClause test) Nothing Nothing Nothing Nothing Nothing)
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem fooBarTable) (whereClause test) Nothing Nothing Nothing Nothing Nothing Nothing)
|
||||
|
||||
Execution.readRows result
|
||||
|
||||
|
60
orville-postgresql/test/Test/Expr/Window.hs
Normal file
60
orville-postgresql/test/Test/Expr/Window.hs
Normal file
@ -0,0 +1,60 @@
|
||||
module Test.Expr.Window
|
||||
( windowTests
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Data.ByteString.Char8 as B8
|
||||
import GHC.Stack (HasCallStack, withFrozenCallStack)
|
||||
import qualified Hedgehog as HH
|
||||
|
||||
import qualified Orville.PostgreSQL.Expr as Expr
|
||||
import qualified Orville.PostgreSQL.Raw.RawSql as RawSql
|
||||
import qualified Orville.PostgreSQL.Raw.SqlValue as SqlValue
|
||||
|
||||
import Test.Expr.TestSchema (barColumn)
|
||||
import qualified Test.Property as Property
|
||||
|
||||
windowTests :: Property.Group
|
||||
windowTests =
|
||||
Property.group
|
||||
"Expr - Window"
|
||||
[ prop_windowClauseSingleDef
|
||||
, prop_windowClauseMultiDef
|
||||
]
|
||||
|
||||
prop_windowClauseSingleDef :: Property.NamedProperty
|
||||
prop_windowClauseSingleDef =
|
||||
let
|
||||
windowDef = Expr.windowDefinition Nothing (Just partitionBy) (Just orderBy) (Just frameClause)
|
||||
windowDefName = Expr.fromIdentifier $ Expr.identifier "a"
|
||||
partitionBy = Expr.partitionBy (pure . Expr.valueExpression $ SqlValue.fromInt32 1)
|
||||
orderBy = Expr.orderByClause $ Expr.orderByColumnName barColumn Expr.ascendingOrder
|
||||
frameClause = Expr.frameClause (Just Expr.rowsFrameMode) Expr.unboundedPrecedingFrameStart (Just Expr.unboundedFollowingFrameEnd) (Just Expr.tiesFrameExclusion)
|
||||
in
|
||||
Property.singletonNamedProperty "window clause with single window definition containing a partition by, an order by, and a frame clause generates expected sql."
|
||||
. assertWindowEquals
|
||||
"WINDOW \"a\" AS (PARTITION BY $1 ORDER BY \"bar\" ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING EXCLUDE TIES)"
|
||||
. Expr.windowClause
|
||||
$ Expr.namedWindowDefinition windowDefName windowDef
|
||||
|
||||
prop_windowClauseMultiDef :: Property.NamedProperty
|
||||
prop_windowClauseMultiDef =
|
||||
let
|
||||
windowDef1 = Expr.windowDefinition Nothing (Just partitionBy) Nothing Nothing
|
||||
windowDef1Name = Expr.fromIdentifier $ Expr.identifier "a"
|
||||
windowDef2 = Expr.windowDefinition (Just windowDef1Name) Nothing (Just orderBy) (Just frameClause)
|
||||
windowDef2Name = Expr.fromIdentifier $ Expr.identifier "b"
|
||||
partitionBy = Expr.partitionBy (pure . Expr.valueExpression $ SqlValue.fromInt32 1)
|
||||
orderBy = Expr.orderByClause $ Expr.orderByColumnName barColumn Expr.ascendingOrder
|
||||
frameClause = Expr.frameClause (Just Expr.rowsFrameMode) Expr.unboundedPrecedingFrameStart (Just Expr.unboundedFollowingFrameEnd) (Just Expr.tiesFrameExclusion)
|
||||
in
|
||||
Property.singletonNamedProperty "window clause with two window definitions, where the second \"copies\" the first, generates expected sql."
|
||||
. assertWindowEquals
|
||||
"WINDOW \"a\" AS (PARTITION BY $1), \"b\" AS (\"a\" ORDER BY \"bar\" ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING EXCLUDE TIES)"
|
||||
. Expr.windowClause
|
||||
$ Expr.namedWindowDefinition windowDef1Name windowDef1 <> Expr.namedWindowDefinition windowDef2Name windowDef2
|
||||
|
||||
assertWindowEquals :: (HH.MonadTest m, HasCallStack) => String -> Expr.WindowClause -> m ()
|
||||
assertWindowEquals windowClauseStr windowClause =
|
||||
withFrozenCallStack $
|
||||
RawSql.toExampleBytes windowClause HH.=== B8.pack windowClauseStr
|
@ -257,7 +257,7 @@ runRoundTripTest pool testCase = do
|
||||
Expr.queryExpr
|
||||
(Expr.selectClause $ Expr.selectExpr Nothing)
|
||||
(Expr.selectColumns [Marshall.fieldColumnName Nothing fieldDef])
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem testTable) Nothing Nothing Nothing Nothing Nothing Nothing)
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem testTable) Nothing Nothing Nothing Nothing Nothing Nothing Nothing)
|
||||
|
||||
Execution.readRows result
|
||||
|
||||
@ -302,7 +302,7 @@ runNullableRoundTripTest pool testCase = do
|
||||
Expr.queryExpr
|
||||
(Expr.selectClause $ Expr.selectExpr Nothing)
|
||||
(Expr.selectColumns [Marshall.fieldColumnName Nothing fieldDef])
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem testTable) Nothing Nothing Nothing Nothing Nothing Nothing)
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem testTable) Nothing Nothing Nothing Nothing Nothing Nothing Nothing)
|
||||
|
||||
Execution.readRows result
|
||||
|
||||
@ -372,7 +372,7 @@ runDefaultValueFieldDefinitionTest pool testCase mkDefaultValue = do
|
||||
Expr.queryExpr
|
||||
(Expr.selectClause $ Expr.selectExpr Nothing)
|
||||
(Expr.selectColumns [Marshall.fieldColumnName Nothing fieldDef])
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem testTable) Nothing Nothing Nothing Nothing Nothing Nothing)
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem testTable) Nothing Nothing Nothing Nothing Nothing Nothing Nothing)
|
||||
|
||||
Execution.readRows result
|
||||
|
||||
|
@ -545,7 +545,7 @@ runDecodingTest pool test =
|
||||
Expr.queryExpr
|
||||
(Expr.selectClause $ Expr.selectExpr Nothing)
|
||||
Expr.selectStar
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem tableName) Nothing Nothing Nothing Nothing Nothing Nothing)
|
||||
(Just $ Expr.tableExpr (Expr.tableFromItem tableName) Nothing Nothing Nothing Nothing Nothing Nothing Nothing)
|
||||
|
||||
Execution.readRows result
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user