orville-postgresql-1.1.0.0: src/Orville/PostgreSQL/Execution/SelectOptions.hs
{- |
Copyright : Flipstone Technology Partners 2023-2025
License : MIT
Stability : Stable
@since 1.0.0.0
-}
module Orville.PostgreSQL.Execution.SelectOptions
( SelectOptions
, emptySelectOptions
, appendSelectOptions
, selectDistinct
, selectWhereClause
, selectOrderByClause
, selectGroupByClause
, selectLimitExpr
, selectOffsetExpr
, selectRowLockingClause
, selectWindowClause
, selectFetchClause
, distinct
, where_
, orderBy
, limit
, offset
, groupBy
, forRowLock
, window
, fetchRow
, selectOptionsQueryExpr
)
where
import Data.Monoid (First (First, getFirst))
import qualified Orville.PostgreSQL.Expr as Expr
{- | A 'SelectOptions' is a set of options that can be used to change the way
a basic query function works by adding @WHERE@, @ORDER BY@, @GROUP BY@, etc.
Functions are provided to construct 'SelectOptions' for individual options,
which may then be combined via '<>' (also exposed as 'appendSelectOptions').
@since 1.0.0.0
-}
data SelectOptions = SelectOptions
{ i_distinct :: First Bool
, i_whereCondition :: Maybe Expr.BooleanExpr
, i_orderBy :: Maybe Expr.OrderByExpr
, i_limitExpr :: First Expr.LimitExpr
, i_offsetExpr :: First Expr.OffsetExpr
, i_groupByExpr :: Maybe Expr.GroupByExpr
, i_rowLockingClause :: First Expr.RowLockingClause
, i_windowExpr :: Maybe Expr.NamedWindowDefinitionExpr
, i_fetchClause :: First Expr.FetchClause
}
-- | @since 1.0.0.0
instance Semigroup SelectOptions where
(<>) = appendSelectOptions
-- | @since 1.0.0.0
instance Monoid SelectOptions where
mempty = emptySelectOptions
{- | A set of empty 'SelectOptions' that will not change how a query is run.
@since 1.0.0.0
-}
emptySelectOptions :: SelectOptions
emptySelectOptions =
SelectOptions
{ i_distinct = mempty
, i_whereCondition = Nothing
, i_orderBy = mempty
, i_limitExpr = mempty
, i_offsetExpr = mempty
, i_groupByExpr = mempty
, i_rowLockingClause = mempty
, i_windowExpr = Nothing
, i_fetchClause = mempty
}
{- | Combines multple select options together, unioning the options together where
possible. For options where this is not possible (e.g. @LIMIT@), the one
on the left is preferred.
@since 1.0.0.0
-}
appendSelectOptions :: SelectOptions -> SelectOptions -> SelectOptions
appendSelectOptions left right =
SelectOptions
(i_distinct left <> i_distinct right)
(unionMaybeWith Expr.andExpr (i_whereCondition left) (i_whereCondition right))
(i_orderBy left <> i_orderBy right)
(i_limitExpr left <> i_limitExpr 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)
(i_fetchClause left <> i_fetchClause right)
unionMaybeWith :: (a -> a -> a) -> Maybe a -> Maybe a -> Maybe a
unionMaybeWith f mbLeft mbRight =
case (mbLeft, mbRight) of
(Nothing, Nothing) -> Nothing
(Just left, Nothing) -> Just left
(Nothing, Just right) -> Just right
(Just left, Just right) -> Just (f left right)
{- | Builds the 'Expr.SelectClause' that should be used to include the
'distinct's from the 'SelectOptions' on a query.
@since 1.0.0.0
-}
selectDistinct :: SelectOptions -> Expr.SelectClause
selectDistinct selectOptions =
case i_distinct selectOptions of
First (Just True) -> Expr.selectClause . Expr.selectExpr $ Just Expr.Distinct
_ -> Expr.selectClause $ Expr.selectExpr Nothing
{- | Builds the 'Expr.WhereClause' that should be used to include the
'Expr.BooleanExpr's from the 'SelectOptions' on a query. This will be 'Nothing'
when no 'Expr.BooleanExpr's have been specified.
@since 1.0.0.0
-}
selectWhereClause :: SelectOptions -> Maybe Expr.WhereClause
selectWhereClause =
fmap Expr.whereClause . i_whereCondition
{- | Constructs a 'SelectOptions' with just 'distinct' set to 'True'.
@since 1.0.0.0
-}
distinct :: SelectOptions
distinct =
emptySelectOptions
{ i_distinct = First $ Just True
}
{- | Builds the 'Expr.OrderByClause' that should be used to include the
'Expr.OrderByClause's from the 'SelectOptions' on a query. This will be
'Nothing' when no 'Expr.OrderByClause's have been specified.
@since 1.0.0.0
-}
selectOrderByClause :: SelectOptions -> Maybe Expr.OrderByClause
selectOrderByClause =
fmap Expr.orderByClause . i_orderBy
{- | Builds the 'Expr.GroupByClause' that should be used to include the
'Expr.GroupByClause's from the 'SelectOptions' on a query. This will be
'Nothing' when no 'Expr.GroupByClause's have been specified.
@since 1.0.0.0
-}
selectGroupByClause :: SelectOptions -> Maybe Expr.GroupByClause
selectGroupByClause =
fmap Expr.groupByClause . i_groupByExpr
{- | Builds a 'Expr.LimitExpr' that will limit the query results to the
number specified in the 'SelectOptions' (if any).
@since 1.0.0.0
-}
selectLimitExpr :: SelectOptions -> Maybe Expr.LimitExpr
selectLimitExpr =
getFirst . i_limitExpr
{- | Builds an 'Expr.OffsetExpr' that will limit the query results to the
number specified in the 'SelectOptions' (if any).
@since 1.0.0.0
-}
selectOffsetExpr :: SelectOptions -> Maybe Expr.OffsetExpr
selectOffsetExpr =
getFirst . i_offsetExpr
{- | Builds an 'Expr.RowLockingClause' that will apply the locking rules specified in the 'SelectOptions' (if any).
@since 1.1.0.0
-}
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
{- | Builds an 'Expr.FetchClause' that will apply the fetching rules specified in the 'SelectOptions' (if any).
@since 1.1.0.0
-}
selectFetchClause :: SelectOptions -> Maybe Expr.FetchClause
selectFetchClause =
getFirst . i_fetchClause
{- | Constructs a 'SelectOptions' with just the given 'Expr.BooleanExpr'.
@since 1.0.0.0
-}
where_ :: Expr.BooleanExpr -> SelectOptions
where_ condition =
emptySelectOptions
{ i_whereCondition = Just condition
}
{- | Constructs a 'SelectOptions' with just the given 'Expr.OrderByExpr'.
@since 1.0.0.0
-}
orderBy :: Expr.OrderByExpr -> SelectOptions
orderBy order =
emptySelectOptions
{ i_orderBy = Just order
}
{- | Constructs a 'SelectOptions' that will apply the given limit.
@since 1.0.0.0
-}
limit :: Int -> SelectOptions
limit limitValue =
emptySelectOptions
{ i_limitExpr = First . Just . Expr.limitExpr $ limitValue
}
{- | Constructs a 'SelectOptions' that will apply the given offset.
@since 1.0.0.0
-}
offset :: Int -> SelectOptions
offset offsetValue =
emptySelectOptions
{ i_offsetExpr = First . Just . Expr.offsetExpr $ offsetValue
}
{- | Constructs a 'SelectOptions' with just the given 'Expr.GroupByExpr'.
@since 1.0.0.0
-}
groupBy :: Expr.GroupByExpr -> SelectOptions
groupBy groupByExpr =
emptySelectOptions
{ i_groupByExpr = Just groupByExpr
}
{- | Constructs a 'SelectOptions' with just the given 'Expr.RowLockingClause'.
@since 1.1.0.0
-}
forRowLock :: Expr.RowLockingClause -> SelectOptions
forRowLock rowLockingClause =
emptySelectOptions
{ 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
}
{- | Constructs a 'SelectOptions' with just the given 'Expr.FetchClause'.
@since 1.1.0.0
-}
fetchRow :: Expr.FetchClause -> SelectOptions
fetchRow fetchClause =
emptySelectOptions
{ i_fetchClause = First $ Just fetchClause
}
{- | 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
sure that the 'Expr.SelectList' expression makes sense for the table being
queried, and that the names of the columns in the result set match those
expected by the 'Orville.PostgreSQL.SqlMarshaller' that is ultimately used to
decode it.
This function is useful for building more advanced queries that need to
select things other than simple columns from the table, such as using
aggregate functions. The 'Expr.SelectList' can be built however the caller
desires. If Orville does not support building the 'Expr.SelectList' you need
using any of the expression-building functions, you can resort to
@RawSql.fromRawSql@ as an escape hatch to build the 'Expr.SelectList' here.
@since 1.0.0.0
-}
selectOptionsQueryExpr ::
Expr.SelectList ->
Expr.TableReferenceList ->
SelectOptions ->
Expr.QueryExpr
selectOptionsQueryExpr selectList tableReferenceList selectOptions =
Expr.queryExpr
(selectDistinct selectOptions)
selectList
( Just $
Expr.tableExpr
tableReferenceList
(selectWhereClause selectOptions)
(selectGroupByClause selectOptions)
(selectOrderByClause selectOptions)
(selectLimitExpr selectOptions)
(selectOffsetExpr selectOptions)
(selectRowLockingClause selectOptions)
(selectWindowClause selectOptions)
(selectFetchClause selectOptions)
)