{-# LANGUAGE UndecidableInstances, FunctionalDependencies #-} module Database.Beam.Query.Combinators ( all_, join_, guard_, related_, relatedBy_, lookup_ , leftJoin_, perhapsAll_ , SqlReferences(..) , SqlJustable(..) , SqlDeconstructMaybe(..) , SqlOrderable , limit_, offset_ , exists_ , (<.), (>.), (<=.), (>=.), (==.), (&&.), (||.), not_, div_, mod_ , HaskellLiteralForQExpr(..), SqlValable(..) -- * SQL GROUP BY and aggregation , aggregate, SqlGroupable(..) , sum_, count_ -- * SQL ORDER BY , orderBy, asc_, desc_ -- * SQL subqueries , subquery_ ) where import Database.Beam.Query.Internal import Database.Beam.Query.Types import Database.Beam.Schema.Tables import Database.Beam.Schema.Fields import Database.Beam.SQL import Database.HDBC import Control.Monad.State import Control.Monad.RWS import Control.Monad.Identity import Data.Monoid import Data.String import Data.Maybe import Data.Proxy import Data.Convertible import Data.Text (Text) import Data.Coerce instance IsString (QExpr Text) where fromString = QExpr . SQLValE . SqlString instance (Num a, Convertible a SqlValue) => Num (QExpr a) where fromInteger x = let res :: QExpr a res = val_ (fromInteger x) in res QExpr a + QExpr b = QExpr (SQLBinOpE "+" a b) QExpr a - QExpr b = QExpr (SQLBinOpE "-" a b) QExpr a * QExpr b = QExpr (SQLBinOpE "*" a b) negate (QExpr a) = QExpr (SQLUnOpE "-" a) abs (QExpr x) = QExpr (SQLFuncE "ABS" [x]) signum x = error "signum: not defined for QExpr. Use CASE...WHEN" instance IsString (Aggregation Text) where fromString = ProjectAgg . SQLValE . SqlString instance (Num a, Convertible a SqlValue) => Num (Aggregation a) where fromInteger x = ProjectAgg (SQLValE (convert (fromInteger x :: a))) ProjectAgg a + ProjectAgg b = ProjectAgg (SQLBinOpE "+" a b) ProjectAgg a - ProjectAgg b = ProjectAgg (SQLBinOpE "-" a b) ProjectAgg a * ProjectAgg b = ProjectAgg (SQLBinOpE "*" a b) negate (ProjectAgg a) = ProjectAgg (SQLUnOpE "-" a) abs (ProjectAgg x) = ProjectAgg (SQLFuncE "ABS" [x]) signum x = error "signum: not defined for Aggregation. Use CASE...WHEN" -- | Introduce all entries of a table into the 'Q' monad all_ :: Database db => DatabaseTable db table -> Q db s (table QExpr) all_ tbl = join_ tbl (val_ True) -- | Introduce all entries of a table into the 'Q' monad based on the given SQLExpr join_ :: Database db => DatabaseTable db table -> QExpr Bool -> Q db s (table QExpr) join_ (DatabaseTable table name :: DatabaseTable db table) on = do curTbl <- gets qbNextTblRef modify $ \qb@QueryBuilder { qbNextTblRef = curTbl , qbFrom = from , qbWhere = where_ } -> let (from', where') = case from of Nothing -> (Just newSource, where_ &&. on) Just from -> ( Just (SQLJoin SQLInnerJoin from newSource (optimizeExpr on)), where_ ) newSource = SQLFromSource (SQLAliased (SQLSourceTable name) (Just (fromString ("t" <> show curTbl)))) in qb { qbNextTblRef = curTbl + 1 , qbFrom = from' , qbWhere = where' } let tableSettings :: TableSettings table tableSettings = tblFieldSettings mkScopedField :: Columnar' (TableField table) a -> Columnar' QExpr a mkScopedField (Columnar' f) = Columnar' (QExpr (SQLFieldE (QField name (Just curTbl) (_fieldName f)))) pure (changeRep mkScopedField tableSettings) -- | Introduce a table using a left join. Because this is not an inner join, the resulting table is -- made nullable. This means that each field that would normally have type 'QExpr x' will now have -- type 'QExpr (Maybe x)'. leftJoin_ :: Database db => DatabaseTable db table -> QExpr Bool -> Q db s (table (Nullable QExpr)) leftJoin_ (DatabaseTable table name :: DatabaseTable db table) on = do curTbl <- gets qbNextTblRef modify $ \qb@QueryBuilder { qbNextTblRef = curTbl , qbFrom = from } -> let from' = case from of Nothing -> error "leftJoin_: empty select source" Just from -> SQLJoin SQLLeftJoin from newSource (optimizeExpr on) newSource = SQLFromSource (SQLAliased (SQLSourceTable name) (Just (fromString ("t" <> show curTbl)))) in qb { qbNextTblRef = curTbl + 1 , qbFrom = Just from' } let tableSettings :: TableSettings table tableSettings = tblFieldSettings mkScopedField :: Columnar' (TableField table) a -> Columnar' (Nullable QExpr) a mkScopedField (Columnar' f) = Columnar' (QExpr (SQLFieldE (QField name (Just curTbl) (_fieldName f)))) pure (changeRep mkScopedField tableSettings) -- | Only allow results for which the 'QExpr' yields 'True' guard_ :: QExpr Bool -> Q db s () guard_ guardE' = modify $ \qb@QueryBuilder { qbWhere = guardE } -> qb { qbWhere = guardE &&. guardE' } -- | Introduce all entries of the given table which are referenced by the given 'PrimaryKey' related_ :: (Database db, Table rel) => DatabaseTable db rel -> PrimaryKey rel QExpr -> Q db s (rel QExpr) related_ (relTbl :: DatabaseTable db rel) pk = mdo rel <- join_ relTbl (pk ==. primaryKey rel) pure rel -- | Introduce all entries of the given table which for which the expression (which can depend on the queried table returns true) relatedBy_ :: (Database db, Table rel) => DatabaseTable db rel -> (rel QExpr -> QExpr Bool) -> Q db s (rel QExpr) relatedBy_ (relTbl :: DatabaseTable db rel) mkOn = mdo rel <- join_ relTbl (mkOn rel) pure rel -- | Introduce related entries of the given table, or if no related entries exist, introduce the null table perhapsAll_ :: (Database db, Table rel) => DatabaseTable db rel -> (rel (Nullable QExpr) -> QExpr Bool) -> Q db s (rel (Nullable QExpr)) perhapsAll_ relTbl expr = mdo rel <- leftJoin_ relTbl (expr rel) pure rel -- | Synonym for 'related_' lookup_ :: (Database db, Table rel) => DatabaseTable db rel -> PrimaryKey rel QExpr -> Q db s (rel QExpr) lookup_ = related_ class SqlReferences f where -- | Check that the 'PrimaryKey' given matches the table. Polymorphic so it works over both -- regular tables and those that have been made nullable by 'leftJoin_'. references_ :: Table tbl => PrimaryKey tbl f -> tbl f -> QExpr Bool instance SqlReferences QExpr where references_ pk (tbl :: tbl QExpr) = pk ==. primaryKey tbl instance SqlReferences (Nullable QExpr) where references_ pk (tbl :: tbl (Nullable QExpr)) = pk ==. primaryKey tbl -- | Limit the number of results returned by a query. -- -- The resulting query is a top-level one that must be passed to 'query', 'queryList', or 'subquery_'. See 'TopLevelQ' for details. limit_ :: IsQuery q => Integer -> q db s a -> TopLevelQ db s a limit_ limit' q = TopLevelQ $ do res <- toQ q modify $ \qb -> let qbLimit' = case qbLimit qb of Nothing -> Just limit' Just limit -> Just (min limit limit') in qb { qbLimit = qbLimit' } pure res -- | Drop the first `offset'` results. -- -- The resulting query is a top-level one that must be passed to 'query', 'queryList', or 'subquery_'. See 'TopLevelQ' for details. offset_ :: IsQuery q => Integer -> q db s a -> TopLevelQ db s a offset_ offset' q = TopLevelQ $ do res <- toQ q modify $ \qb -> let qbOffset' = case qbOffset qb of Nothing -> Just offset' Just offset -> Just (offset + offset') in qb { qbOffset = qbOffset' } pure res -- | Use the SQL exists operator to determine if the given query returns any results exists_ :: (IsQuery q, Projectible a) => q db s a -> QExpr Bool exists_ q = let (_, _, selectCmd) = queryToSQL' (toQ q) 0 in QExpr (SQLExistsE selectCmd) -- ** Combinators for boolean expressions class SqlOrd a where (==.), (/=.) :: a -> a -> QExpr Bool a /=. b = not_ (a ==. b) instance SqlOrd (QExpr a) where (==.) = binOpE "==" (/=.) = binOpE "<>" newtype QExprBool a = QExprBool (QExpr Bool) instance {-# OVERLAPPING #-} Table tbl => SqlOrd (PrimaryKey tbl QExpr) where a ==. b = let pkCmp = runIdentity (zipPkM (\(Columnar' x) (Columnar' y) -> return (Columnar' (QExprBool (x ==. y))) ) a b) :: PrimaryKey tbl QExprBool in foldr (&&.) (val_ True) (pkAllValues (\(Columnar' (QExprBool x)) -> x) pkCmp) instance {-# OVERLAPPING #-} Table tbl => SqlOrd (tbl QExpr) where a ==. b = let tblCmp = runIdentity (zipTablesM (\(Columnar' x) (Columnar' y) -> return (Columnar' (QExprBool (x ==. y))) ) a b) :: tbl QExprBool in foldr (&&.) (val_ True) (fieldAllValues (\(Columnar' (QExprBool x)) -> x) tblCmp) instance {-# OVERLAPPING #-} Table tbl => SqlOrd (PrimaryKey tbl (Nullable QExpr)) where a ==. b = let pkCmp = runIdentity (zipPkM (\(Columnar' x) (Columnar' y) -> return (Columnar' (QExprBool (x ==. y))) ) a b) :: PrimaryKey tbl QExprBool in foldr (&&.) (val_ True) (pkAllValues (\(Columnar' (QExprBool x)) -> x) pkCmp) instance {-# OVERLAPPING #-} Table tbl => SqlOrd (tbl (Nullable QExpr)) where a ==. b = let tblCmp = runIdentity (zipTablesM (\(Columnar' x) (Columnar' y) -> return (Columnar' (QExprBool (x ==. y))) ) a b) :: tbl QExprBool in foldr (&&.) (val_ True) (fieldAllValues (\(Columnar' (QExprBool x)) -> x) tblCmp) binOpE op (QExpr a) (QExpr b) = QExpr (SQLBinOpE op a b) (<.), (>.), (<=.), (>=.) :: QExpr a -> QExpr a -> QExpr Bool (<.) = binOpE "<" (>.) = binOpE ">" (<=.) = binOpE "<=" (>=.) = binOpE ">=" (&&.), (||.) :: QExpr Bool -> QExpr Bool -> QExpr Bool (&&.) = binOpE "AND" (||.) = binOpE "OR" infixr 3 &&. infixr 2 ||. infix 4 ==., /=. not_ :: QExpr Bool -> QExpr Bool not_ (QExpr a) = QExpr (SQLUnOpE "NOT" a) mod_, div_ :: Integral a => QExpr a -> QExpr a -> QExpr a div_ = binOpE "/" mod_ = binOpE "%" -- * Marshalling between Haskell literals and QExprs type family HaskellLiteralForQExpr x type instance HaskellLiteralForQExpr (QExpr a) = a type instance HaskellLiteralForQExpr (table QExpr) = table Identity class SqlValable a where val_ :: HaskellLiteralForQExpr a -> a instance Convertible a SqlValue => SqlValable (QExpr a) where val_ = QExpr . SQLValE . convert -- NOTE: This shouldn't cause problems because both overlapping instances are in the same module. -- GHC should prefer the PrimaryKey one for primary keys and the table one for everything else. -- AFAICT, PrimaryKey tbl QExpr ~ tbl QExpr is impossible instance {-# OVERLAPPING #-} Table tbl => SqlValable (PrimaryKey tbl QExpr) where val_ = pkChangeRep valToQExpr . pkMakeSqlValues where valToQExpr :: Columnar' SqlValue' a -> Columnar' QExpr a valToQExpr (Columnar' (SqlValue' v)) = Columnar' (QExpr (SQLValE v)) instance {-# OVERLAPPING #-} Table tbl => SqlValable (tbl QExpr) where val_ = changeRep valToQExpr . makeSqlValues where valToQExpr :: Columnar' SqlValue' a -> Columnar' QExpr a valToQExpr (Columnar' (SqlValue' v)) = Columnar' (QExpr (SQLValE v)) -- * Aggregators class Aggregating agg where type LiftAggregationsToQExpr agg aggToSql :: agg -> SQLGrouping liftAggToQExpr :: agg -> LiftAggregationsToQExpr agg instance Table t => Aggregating (t Aggregation) where type LiftAggregationsToQExpr (t Aggregation) = t QExpr aggToSql table = mconcat (fieldAllValues (\(Columnar' x) -> aggToSql x) table) liftAggToQExpr = changeRep (\(Columnar' x) -> Columnar' (liftAggToQExpr x)) instance Aggregating (Aggregation a) where type LiftAggregationsToQExpr (Aggregation a) = QExpr a aggToSql (GroupAgg e) = let eSql = optimizeExpr' e in mempty { sqlGroupBy = [eSql] } aggToSql (ProjectAgg _) = mempty liftAggToQExpr (GroupAgg e) = QExpr e liftAggToQExpr (ProjectAgg e) = QExpr e instance (Aggregating a, Aggregating b) => Aggregating (a, b) where type LiftAggregationsToQExpr (a, b) = ( LiftAggregationsToQExpr a , LiftAggregationsToQExpr b ) aggToSql (a, b) = aggToSql a <> aggToSql b liftAggToQExpr (a, b) = (liftAggToQExpr a, liftAggToQExpr b) instance (Aggregating a, Aggregating b, Aggregating c) => Aggregating (a, b, c) where type LiftAggregationsToQExpr (a, b, c) = ( LiftAggregationsToQExpr a , LiftAggregationsToQExpr b , LiftAggregationsToQExpr c ) aggToSql (a, b, c) = aggToSql a <> aggToSql b <> aggToSql c liftAggToQExpr (a, b, c) = (liftAggToQExpr a, liftAggToQExpr b, liftAggToQExpr c) instance (Aggregating a, Aggregating b, Aggregating c, Aggregating d) => Aggregating (a, b, c, d) where type LiftAggregationsToQExpr (a, b, c, d) = ( LiftAggregationsToQExpr a , LiftAggregationsToQExpr b , LiftAggregationsToQExpr c , LiftAggregationsToQExpr d ) aggToSql (a, b, c, d) = aggToSql a <> aggToSql b <> aggToSql c <> aggToSql d liftAggToQExpr (a, b, c, d) = (liftAggToQExpr a, liftAggToQExpr b, liftAggToQExpr c, liftAggToQExpr d) instance (Aggregating a, Aggregating b, Aggregating c, Aggregating d, Aggregating e) => Aggregating (a, b, c, d, e) where type LiftAggregationsToQExpr (a, b, c, d, e) = ( LiftAggregationsToQExpr a , LiftAggregationsToQExpr b , LiftAggregationsToQExpr c , LiftAggregationsToQExpr d , LiftAggregationsToQExpr e ) aggToSql (a, b, c, d, e) = aggToSql a <> aggToSql b <> aggToSql c <> aggToSql d <> aggToSql e liftAggToQExpr (a, b, c, d, e) = (liftAggToQExpr a, liftAggToQExpr b, liftAggToQExpr c, liftAggToQExpr d, liftAggToQExpr e) -- | Type class for things that can be used as the basis of a grouping in a SQL GROUP BY -- clause. This includes 'QExpr a', 'Table's, and 'PrimaryKey's. Because the given object forms the -- basis of the group, its value is available for use in the result set. class SqlGroupable a where type GroupResult a -- | When included in an 'Aggregating' expression, causes the results to be grouped by the -- given column. group_ :: a -> GroupResult a instance SqlGroupable (QExpr a) where type GroupResult (QExpr a) = Aggregation a group_ (QExpr a) = GroupAgg a instance {-# OVERLAPPING #-} Table t => SqlGroupable (PrimaryKey t QExpr) where type GroupResult (PrimaryKey t QExpr) = PrimaryKey t Aggregation group_ = pkChangeRep (\(Columnar' (QExpr e)) -> Columnar' (GroupAgg e)) instance {-# OVERLAPPING #-} Table t => SqlGroupable (t QExpr) where type GroupResult (t QExpr) = t Aggregation group_ = changeRep (\(Columnar' (QExpr e)) -> Columnar' (GroupAgg e)) sum_ :: Num a => QExpr a -> Aggregation a sum_ (QExpr over) = ProjectAgg (SQLFuncE "SUM" [over]) count_ :: QExpr a -> Aggregation Int count_ (QExpr over) = ProjectAgg (SQLFuncE "COUNT" [over]) -- | Return a 'TopLevelQ' that will aggregate over the results of the original query. The -- aggregation function (first argument) should accept the output of the query, and return a member -- of the 'Aggregating' class which will become the result of the new query. See the 'group_' -- combinator as well as the various aggregation combinators ('sum_', 'count_', etc.) -- -- For example, -- -- > aggregate (\employee -> (group_ (_employeeRegion employee), count_ (_employeeId employee))) (all_ employeesTable) -- -- will group the result of the `all_ employeesTable` query using the `_employeeRegion` record -- field, and then count up the number of employees for each region. aggregate :: (Projectible a, Aggregating agg) => (a -> agg) -> (forall s. Q db s a) -> TopLevelQ db s (LiftAggregationsToQExpr agg) aggregate aggregator q = TopLevelQ $ do res <- q curTbl <- gets qbNextTblRef let aggregation = aggregator res grouping' = aggToSql aggregation modify $ \qb -> case sqlGroupBy grouping' of [] -> qb _ -> case qbGrouping qb of Nothing -> qb { qbGrouping = Just grouping' } Just grouping -> qb { qbGrouping = Just (grouping <> grouping') } pure (liftAggToQExpr aggregation) -- * Order bys class SqlOrderable a where makeSQLOrdering :: a -> [SQLOrdering] instance SqlOrderable SQLOrdering where makeSQLOrdering x = [x] instance SqlOrderable a => SqlOrderable [a] where makeSQLOrdering = concatMap makeSQLOrdering instance ( SqlOrderable a , SqlOrderable b ) => SqlOrderable (a, b) where makeSQLOrdering (a, b) = makeSQLOrdering a <> makeSQLOrdering b instance ( SqlOrderable a , SqlOrderable b , SqlOrderable c ) => SqlOrderable (a, b, c) where makeSQLOrdering (a, b, c) = makeSQLOrdering a <> makeSQLOrdering b <> makeSQLOrdering c instance ( SqlOrderable a , SqlOrderable b , SqlOrderable c , SqlOrderable d ) => SqlOrderable (a, b, c, d) where makeSQLOrdering (a, b, c, d) = makeSQLOrdering a <> makeSQLOrdering b <> makeSQLOrdering c <> makeSQLOrdering d instance ( SqlOrderable a , SqlOrderable b , SqlOrderable c , SqlOrderable d , SqlOrderable e ) => SqlOrderable (a, b, c, d, e) where makeSQLOrdering (a, b, c, d, e) = makeSQLOrdering a <> makeSQLOrdering b <> makeSQLOrdering c <> makeSQLOrdering d <> makeSQLOrdering e -- | Order by certain expressions, either ascending ('asc_') or descending ('desc_') orderBy :: (SqlOrderable ordering, IsQuery q) => (a -> ordering) -> q db s a -> TopLevelQ db s a orderBy orderer q = TopLevelQ $ do res <- toQ q let ordering = makeSQLOrdering (orderer res) modify $ \qb -> qb { qbOrdering = qbOrdering qb <> ordering } pure res desc_, asc_ :: QExpr a -> SQLOrdering asc_ e = Asc (optimizeExpr e) desc_ e = Desc (optimizeExpr e) -- * Subqueries class Subqueryable a where subqueryProjections :: a -> RWS (Text, Int) [SQLAliased SQLExpr] Int a instance Subqueryable (QExpr a) where subqueryProjections e = do i <- state (\i -> (i, i+1)) (tblName, tblOrd) <- ask let fieldName = fromString ("e" <> show i) tell [SQLAliased (optimizeExpr e) (Just fieldName)] pure (QExpr (SQLFieldE (QField tblName (Just tblOrd) fieldName))) instance ( Subqueryable a , Subqueryable b ) => Subqueryable (a, b) where subqueryProjections (a, b) = (,) <$> subqueryProjections a <*> subqueryProjections b instance ( Subqueryable a , Subqueryable b , Subqueryable c ) => Subqueryable (a, b, c) where subqueryProjections (a, b, c) = (,,) <$> subqueryProjections a <*> subqueryProjections b <*> subqueryProjections c instance ( Subqueryable a , Subqueryable b , Subqueryable c , Subqueryable d ) => Subqueryable (a, b, c, d) where subqueryProjections (a, b, c, d) = (,,,) <$> subqueryProjections a <*> subqueryProjections b <*> subqueryProjections c <*> subqueryProjections d instance ( Subqueryable a , Subqueryable b , Subqueryable c , Subqueryable d , Subqueryable e ) => Subqueryable (a, b, c, d, e) where subqueryProjections (a, b, c, d, e) = (,,,,) <$> subqueryProjections a <*> subqueryProjections b <*> subqueryProjections c <*> subqueryProjections d <*> subqueryProjections e -- | Run the given 'Q'-like object as a subquery, joining the results with the current result -- set. This allows embedding of 'TopLevelQ's inside 'Q's or other 'TopLevelQ's. subquery_ :: (IsQuery q, Projectible a, Subqueryable a) => (forall s. q db s a) -> Q db s a subquery_ q = do curTbl <- gets qbNextTblRef let (res, curTbl', select') = queryToSQL' (toQ q) curTbl subTblName = fromString ("t" <> show curTbl) (res', projection') = evalRWS (subqueryProjections res) (subTblName, curTbl') 0 select'' = select' { selProjection = SQLProj projection' } modify $ \qb@QueryBuilder { qbFrom = from } -> let from' = case from of Nothing -> Just newSource Just from -> Just (SQLJoin SQLInnerJoin from newSource (SQLValE (SqlBool True))) newSource = SQLFromSource (SQLAliased (SQLSourceSelect select'') (Just (fromString ("t" <> show curTbl')))) in qb { qbNextTblRef = curTbl' + 1 , qbFrom = from' } pure res' -- * Nullable conversions -- | Type class for things that can be nullable. This includes 'QExpr (Maybe a)', 'tbl (Nullable -- QExpr)', and 'PrimaryKey tbl (Nullable QExpr)' class SqlJustable a b | b -> a where -- | Given something of type 'QExpr a', 'tbl QExpr', or 'PrimaryKey tbl QExpr', turn it into a -- 'QExpr (Maybe a)', 'tbl (Nullable QExpr)', or 'PrimaryKey t (Nullable QExpr)' respectively -- that contains the same values. just_ :: a -> b -- | Return either a 'QExpr (Maybe x)' representing 'Nothing' or a nullable 'Table' or -- 'PrimaryKey' filled with 'Nothing'. nothing_ :: b instance SqlJustable (QExpr a) (QExpr (Maybe a)) where just_ (QExpr e) = QExpr e nothing_ = QExpr (SQLValE SqlNull) instance {-# OVERLAPPING #-} Table t => SqlJustable (PrimaryKey t QExpr) (PrimaryKey t (Nullable QExpr)) where just_ = pkChangeRep (\(Columnar' q) -> Columnar' (just_ q)) nothing_ = pkChangeRep (\(Columnar' q) -> Columnar' nothing_) (primaryKey (tblFieldSettings :: TableSettings t)) instance {-# OVERLAPPING #-} Table t => SqlJustable (t QExpr) (t (Nullable QExpr)) where just_ = changeRep (\(Columnar' q) -> Columnar' (just_ q)) nothing_ = changeRep (\(Columnar' q) -> Columnar' nothing_) (tblFieldSettings :: TableSettings t) instance {-# OVERLAPPING #-} Table t => SqlJustable (PrimaryKey t Identity) (PrimaryKey t (Nullable Identity)) where just_ = pkChangeRep (\(Columnar' q) -> Columnar' (Just q)) nothing_ = pkChangeRep (\(Columnar' q) -> Columnar' Nothing) (primaryKey (tblFieldSettings :: TableSettings t)) instance {-# OVERLAPPING #-} Table t => SqlJustable (t Identity) (t (Nullable Identity)) where just_ = changeRep (\(Columnar' q) -> Columnar' (Just q)) nothing_ = changeRep (\(Columnar' q) -> Columnar' Nothing) (tblFieldSettings :: TableSettings t) -- * Nullable checking -- | Type class for anything which can be checked for null-ness. This includes 'QExpr (Maybe a)' as -- well as 'Table's or 'PrimaryKey's over 'Nullable QExpr'. class SqlDeconstructMaybe a nonNullA | a -> nonNullA where -- | Returns a 'QExpr' that evaluates to true when the first argument is not null isJust_ :: a -> QExpr Bool -- | Returns a 'QExpr' that evaluates to true when the first argument is null isNothing_ :: a -> QExpr Bool -- | Given an object (third argument) which may or may not be null, return the default value if -- null (first argument), or transform the value that could be null to yield the result of the -- expression (second argument) maybe_ :: QExpr y -> (nonNullA -> QExpr y) -> a -> QExpr y instance SqlDeconstructMaybe (QExpr (Maybe x)) (QExpr x) where isJust_ (QExpr x) = QExpr (SQLIsJustE x) isNothing_ (QExpr x) = QExpr (SQLIsNothingE x) maybe_ (QExpr onNothing) onJust (QExpr e) = let QExpr onJust' = onJust (QExpr e) in QExpr (SQLCaseE [(SQLIsJustE e, onJust')] onNothing) instance {-# OVERLAPPING #-} Table t => SqlDeconstructMaybe (PrimaryKey t (Nullable QExpr)) (PrimaryKey t QExpr) where isJust_ pk = let fieldsAreJust = pkChangeRep (\(Columnar' x) -> Columnar' (QExprBool (isJust_ x))) pk :: PrimaryKey t QExprBool in foldr (&&.) (val_ True) (pkAllValues (\(Columnar' (QExprBool e)) -> e) fieldsAreJust) isNothing_ pk = let fieldsAreNothing = pkChangeRep (\(Columnar' x) -> Columnar' (QExprBool (isNothing_ x))) pk :: PrimaryKey t QExprBool in foldr (&&.) (val_ True) (pkAllValues (\(Columnar' (QExprBool e)) -> e) fieldsAreNothing) maybe_ = undefined instance {-# OVERLAPPING #-} Table t => SqlDeconstructMaybe (t (Nullable QExpr)) (t QExpr) where isJust_ t = let fieldsAreJust = changeRep (\(Columnar' x) -> Columnar' (QExprBool (isJust_ x))) t :: t QExprBool in foldr (&&.) (val_ True) (fieldAllValues (\(Columnar' (QExprBool e)) -> e) fieldsAreJust) isNothing_ t = let fieldsAreNothing = changeRep (\(Columnar' x) -> Columnar' (QExprBool (isNothing_ x))) t :: t QExprBool in foldr (&&.) (val_ True) (fieldAllValues (\(Columnar' (QExprBool e)) -> e) fieldsAreNothing) maybe_ = undefined class BeamUnwrapMaybe c where beamUnwrapMaybe :: Columnar' (Nullable c) x -> Columnar' c x instance BeamUnwrapMaybe QExpr where beamUnwrapMaybe (Columnar' (QExpr e)) = Columnar' (QExpr e) instance BeamUnwrapMaybe c => BeamUnwrapMaybe (Nullable c) where beamUnwrapMaybe (Columnar' x :: Columnar' (Nullable (Nullable c)) x) = let Columnar' x' = beamUnwrapMaybe (Columnar' x :: Columnar' (Nullable c) (Maybe x)) :: Columnar' c (Maybe x) xCol :: Columnar' (Nullable c) x xCol = Columnar' x' in xCol instance {-# OVERLAPPING #-} ( SqlDeconstructMaybe (PrimaryKey t (Nullable c)) res, Table t, BeamUnwrapMaybe (Nullable c)) => SqlDeconstructMaybe (PrimaryKey t (Nullable (Nullable c))) res where isJust_ t = isJust_ (pkChangeRep (\(f :: Columnar' (Nullable (Nullable c)) x) -> beamUnwrapMaybe f :: Columnar' (Nullable c) x) t) isNothing_ t = isNothing_ (pkChangeRep (\(f :: Columnar' (Nullable (Nullable c)) x) -> beamUnwrapMaybe f :: Columnar' (Nullable c) x) t) maybe_ = undefined instance {-# OVERLAPPING #-} ( SqlDeconstructMaybe (t (Nullable c)) res, Table t, BeamUnwrapMaybe (Nullable c)) => SqlDeconstructMaybe (t (Nullable (Nullable c))) res where isJust_ t = isJust_ (changeRep (\(f :: Columnar' (Nullable (Nullable c)) x) -> beamUnwrapMaybe f :: Columnar' (Nullable c) x) t) isNothing_ t = isNothing_ (changeRep (\(f :: Columnar' (Nullable (Nullable c)) x) -> beamUnwrapMaybe f :: Columnar' (Nullable c) x) t) maybe_ = undefined