{-# LANGUAGE BangPatterns, TypeFamilies, DataKinds, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, UndecidableInstances, StandaloneDeriving, FunctionalDependencies, FlexibleContexts, InstanceSigs, AllowAmbiguousTypes, DeriveAnyClass #-}
{-|
Module: IHP.QueryBuilder.Union
Description: UNION and OR operations for QueryBuilder
Copyright: (c) digitally induced GmbH, 2020

This module provides functions for combining queries with UNION and OR operations.
-}
module IHP.QueryBuilder.Union
( queryUnion
, queryUnionList
, queryOr
) where

import IHP.Prelude
import IHP.ModelSupport
import IHP.QueryBuilder.Types
import IHP.QueryBuilder.Compiler (query)

-- | Merges the results of two query builders by ORing their WHERE conditions.
--
-- Take a look at 'queryOr'  as well, as this might be a bit shorter.
--
-- __Example:__ Return all pages owned by the user or owned by the users team.
--
-- > let userPages = query @Page |> filterWhere (#ownerId, currentUserId)
-- > let teamPages = query @Page |> filterWhere (#teamId, currentTeamId)
-- > pages <- queryUnion userPages teamPages |> fetch
-- > -- SELECT * FROM pages WHERE (owner_id = '..') OR (team_id = '..')
queryUnion :: QueryBuilder model -> QueryBuilder model -> QueryBuilder model
queryUnion :: forall (model :: Symbol).
QueryBuilder model -> QueryBuilder model -> QueryBuilder model
queryUnion (QueryBuilder SQLQuery
first) (QueryBuilder SQLQuery
second) =
    let isSimple :: SQLQuery -> Bool
isSimple SQLQuery
q = [OrderByClause] -> Bool
forall mono. MonoFoldable mono => mono -> Bool
null (SQLQuery -> [OrderByClause]
orderByClause SQLQuery
q) Bool -> Bool -> Bool
&& Maybe Int -> Bool
forall a. Maybe a -> Bool
isNothing (SQLQuery -> Maybe Int
limitClause SQLQuery
q) Bool -> Bool -> Bool
&& Maybe Int -> Bool
forall a. Maybe a -> Bool
isNothing (SQLQuery -> Maybe Int
offsetClause SQLQuery
q)
        unionWhere :: Maybe Condition
unionWhere = case (SQLQuery -> Maybe Condition
whereCondition SQLQuery
first, SQLQuery -> Maybe Condition
whereCondition SQLQuery
second) of
            (Maybe Condition
Nothing, Maybe Condition
wc) -> Maybe Condition
wc
            (Maybe Condition
wc, Maybe Condition
Nothing) -> Maybe Condition
wc
            (Just Condition
a, Just Condition
b) -> Condition -> Maybe Condition
forall a. a -> Maybe a
Just (Condition -> Condition -> Condition
OrCondition Condition
a Condition
b)
    in if SQLQuery -> Bool
isSimple SQLQuery
first Bool -> Bool -> Bool
&& SQLQuery -> Bool
isSimple SQLQuery
second
        then SQLQuery -> QueryBuilder model
forall (table :: Symbol). SQLQuery -> QueryBuilder table
QueryBuilder SQLQuery
first { whereCondition = unionWhere }
        else Text -> QueryBuilder model
forall a. Text -> a
error Text
"queryUnion: Union of complex queries (with ORDER BY, LIMIT, or OFFSET) not supported"
{-# INLINE queryUnion #-}

-- | Like 'queryUnion', but applied on all the elements on the list
--
-- >  action ProjectsAction = do
-- >      let values :: [(ProjectType, Int)] = [(ProjectTypeOngoing, 3), (ProjectTypeNotStarted, 2)]
-- >
-- >          valuePairToCondition :: (ProjectType, Int) -> QueryBuilder "projects"
-- >          valuePairToCondition (projectType, participants) =
-- >              query @Project
-- >                  |> filterWhere (#projectType, projectType)
-- >                  |> filterWhere (#participants, participants)
-- >
-- >          theQuery = queryUnionList (map valuePairToCondition values)
-- >
-- >      projects <- fetch theQuery
-- >      render IndexView { .. }
queryUnionList :: forall table. (Table (GetModelByTableName table), KnownSymbol table, GetTableName (GetModelByTableName table) ~ table) => [QueryBuilder table] -> QueryBuilder table
-- For empty list, create a condition that is always false: id <> id (which is always false for non-null)
queryUnionList :: forall (table :: Symbol).
(Table (GetModelByTableName table), KnownSymbol table,
 GetTableName (GetModelByTableName table) ~ table) =>
[QueryBuilder table] -> QueryBuilder table
queryUnionList [] = Condition -> QueryBuilder table -> QueryBuilder table
forall (table :: Symbol).
Condition -> QueryBuilder table -> QueryBuilder table
addCondition (Text
-> FilterOperator
-> ConditionValue
-> Maybe Text
-> Maybe Text
-> Condition
ColumnCondition Text
"id" FilterOperator
NotEqOp (Text -> ConditionValue
Literal Text
"id") Maybe Text
forall a. Maybe a
Nothing Maybe Text
forall a. Maybe a
Nothing) (forall model (table :: Symbol).
(table ~ GetTableName model, Table model, DefaultScope table) =>
QueryBuilder table
query @(GetModelByTableName table) @table)
queryUnionList [QueryBuilder table
single] = QueryBuilder table
single
queryUnionList (QueryBuilder table
first:[QueryBuilder table]
rest) =
    let QueryBuilder SQLQuery
firstSq = QueryBuilder table
first
        QueryBuilder SQLQuery
restSq = forall (table :: Symbol).
(Table (GetModelByTableName table), KnownSymbol table,
 GetTableName (GetModelByTableName table) ~ table) =>
[QueryBuilder table] -> QueryBuilder table
queryUnionList @table [QueryBuilder table]
rest
        isSimple :: SQLQuery -> Bool
isSimple SQLQuery
q = [OrderByClause] -> Bool
forall mono. MonoFoldable mono => mono -> Bool
null (SQLQuery -> [OrderByClause]
orderByClause SQLQuery
q) Bool -> Bool -> Bool
&& Maybe Int -> Bool
forall a. Maybe a -> Bool
isNothing (SQLQuery -> Maybe Int
limitClause SQLQuery
q) Bool -> Bool -> Bool
&& Maybe Int -> Bool
forall a. Maybe a -> Bool
isNothing (SQLQuery -> Maybe Int
offsetClause SQLQuery
q)
        unionWhere :: Maybe Condition
unionWhere = case (SQLQuery -> Maybe Condition
whereCondition SQLQuery
firstSq, SQLQuery -> Maybe Condition
whereCondition SQLQuery
restSq) of
            (Maybe Condition
Nothing, Maybe Condition
wc) -> Maybe Condition
wc
            (Maybe Condition
wc, Maybe Condition
Nothing) -> Maybe Condition
wc
            (Just Condition
a, Just Condition
b) -> Condition -> Maybe Condition
forall a. a -> Maybe a
Just (Condition -> Condition -> Condition
OrCondition Condition
a Condition
b)
    in if SQLQuery -> Bool
isSimple SQLQuery
firstSq Bool -> Bool -> Bool
&& SQLQuery -> Bool
isSimple SQLQuery
restSq
        then SQLQuery -> QueryBuilder table
forall (table :: Symbol). SQLQuery -> QueryBuilder table
QueryBuilder SQLQuery
firstSq { whereCondition = unionWhere }
        else Text -> QueryBuilder table
forall a. Text -> a
error Text
"queryUnionList: Union of complex queries (with ORDER BY, LIMIT, or OFFSET) not supported"


-- | Adds an @a OR b@ condition
--
-- __Example:__ Return all pages owned by the user or public.
--
-- > query @Page
-- >     |> queryOr
-- >         (filterWhere (#createdBy, currentUserId))
-- >         (filterWhere (#public, True))
-- >     |> fetch
-- > -- SELECT * FROM pages WHERE created_by = '..' OR public = True
queryOr :: (QueryBuilder model -> QueryBuilder model) -> (QueryBuilder model -> QueryBuilder model) -> QueryBuilder model -> QueryBuilder model
queryOr :: forall (model :: Symbol).
(QueryBuilder model -> QueryBuilder model)
-> (QueryBuilder model -> QueryBuilder model)
-> QueryBuilder model
-> QueryBuilder model
queryOr QueryBuilder model -> QueryBuilder model
firstQuery QueryBuilder model -> QueryBuilder model
secondQuery QueryBuilder model
queryBuilder =
    let QueryBuilder SQLQuery
firstSq = QueryBuilder model -> QueryBuilder model
firstQuery QueryBuilder model
queryBuilder
        QueryBuilder SQLQuery
secondSq = QueryBuilder model -> QueryBuilder model
secondQuery QueryBuilder model
queryBuilder
        unionWhere :: Maybe Condition
unionWhere = case (SQLQuery -> Maybe Condition
whereCondition SQLQuery
firstSq, SQLQuery -> Maybe Condition
whereCondition SQLQuery
secondSq) of
            (Maybe Condition
Nothing, Maybe Condition
wc) -> Maybe Condition
wc
            (Maybe Condition
wc, Maybe Condition
Nothing) -> Maybe Condition
wc
            (Just Condition
a, Just Condition
b) -> Condition -> Maybe Condition
forall a. a -> Maybe a
Just (Condition -> Condition -> Condition
OrCondition Condition
a Condition
b)
        QueryBuilder SQLQuery
baseSq = QueryBuilder model
queryBuilder
    in SQLQuery -> QueryBuilder model
forall (table :: Symbol). SQLQuery -> QueryBuilder table
QueryBuilder SQLQuery
baseSq { whereCondition = unionWhere }
{-# INLINE queryOr #-}