ihp-1.4.0: IHP/Fetch.hs
{-# LANGUAGE BangPatterns, TypeFamilies, DataKinds, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, UndecidableInstances, StandaloneDeriving, FunctionalDependencies, FlexibleContexts, InstanceSigs, AllowAmbiguousTypes, DeriveAnyClass #-}
{-|
Module: IHP.Fetch
Description: fetch, fetchOne, fetchOneOrNothing and friends
Copyright: (c) digitally induced GmbH, 2020
This modules builds on top of 'IHP.QueryBuilder' and provides functions to fetch a query builder.
For more complex sql queries, use 'IHP.ModelSupport.sqlQuery'.
-}
module IHP.Fetch
( findManyBy
, findMaybeBy
, findBy
, In (In)
, genericFetchId
, genericfetchIdOneOrNothing
, genericFetchIdOne
, Fetchable (..)
, genericFetchIds
, genericfetchIdsOneOrNothing
, genericFetchIdsOne
, fetchCount
, fetchExists
, fetchSQLQuery
, fetchLatest
, fetchLatestBy
)
where
import IHP.Prelude
import Database.PostgreSQL.Simple.Types (Query (Query))
import Database.PostgreSQL.Simple.FromField hiding (Field, name)
import Database.PostgreSQL.Simple.ToField
import qualified Database.PostgreSQL.Simple as PG
import IHP.ModelSupport
import IHP.QueryBuilder
class Fetchable fetchable model | fetchable -> model where
type FetchResult fetchable model
fetch :: (Table model, PG.FromRow model, ?modelContext :: ModelContext) => fetchable -> IO (FetchResult fetchable model)
fetchOneOrNothing :: (Table model, PG.FromRow model, ?modelContext :: ModelContext) => fetchable -> IO (Maybe model)
fetchOne :: (Table model, PG.FromRow model, ?modelContext :: ModelContext) => fetchable -> IO model
-- The instance declaration had to be split up because a type variable ranging over HasQueryBuilder instances is not allowed in the declaration of the associated type. The common*-functions reduce the redundancy to the necessary minimum.
instance (model ~ GetModelByTableName table, KnownSymbol table) => Fetchable (QueryBuilder table) model where
type instance FetchResult (QueryBuilder table) model = [model]
{-# INLINE fetch #-}
fetch :: (Table model, PG.FromRow model, ?modelContext :: ModelContext) => QueryBuilder table -> IO [model]
fetch = commonFetch
{-# INLINE fetchOneOrNothing #-}
fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => QueryBuilder table -> IO (Maybe model)
fetchOneOrNothing = commonFetchOneOrNothing
{-# INLINE fetchOne #-}
fetchOne :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => QueryBuilder table -> IO model
fetchOne = commonFetchOne
instance (model ~ GetModelByTableName table, KnownSymbol table) => Fetchable (JoinQueryBuilderWrapper r table) model where
type instance FetchResult (JoinQueryBuilderWrapper r table) model = [model]
{-# INLINE fetch #-}
fetch :: (Table model, PG.FromRow model, ?modelContext :: ModelContext) => JoinQueryBuilderWrapper r table -> IO [model]
fetch = commonFetch
{-# INLINE fetchOneOrNothing #-}
fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => JoinQueryBuilderWrapper r table -> IO (Maybe model)
fetchOneOrNothing = commonFetchOneOrNothing
{-# INLINE fetchOne #-}
fetchOne :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => JoinQueryBuilderWrapper r table -> IO model
fetchOne = commonFetchOne
instance (model ~ GetModelByTableName table, KnownSymbol table) => Fetchable (NoJoinQueryBuilderWrapper table) model where
type instance FetchResult (NoJoinQueryBuilderWrapper table) model = [model]
{-# INLINE fetch #-}
fetch :: (Table model, PG.FromRow model, ?modelContext :: ModelContext) => NoJoinQueryBuilderWrapper table -> IO [model]
fetch = commonFetch
{-# INLINE fetchOneOrNothing #-}
fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => NoJoinQueryBuilderWrapper table -> IO (Maybe model)
fetchOneOrNothing = commonFetchOneOrNothing
{-# INLINE fetchOne #-}
fetchOne :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => NoJoinQueryBuilderWrapper table -> IO model
fetchOne = commonFetchOne
instance (model ~ GetModelByTableName table, KnownSymbol table, FromField value, KnownSymbol foreignTable, foreignModel ~ GetModelByTableName foreignTable, KnownSymbol columnName, HasField columnName foreignModel value, HasQueryBuilder (LabeledQueryBuilderWrapper foreignTable columnName value) NoJoins) => Fetchable (LabeledQueryBuilderWrapper foreignTable columnName value table) model where
type instance FetchResult (LabeledQueryBuilderWrapper foreignTable columnName value table) model = [LabeledData value model]
-- fetch needs to return a list of labeled data. The
{-# INLINE fetch #-}
fetch :: (Table model, PG.FromRow model, ?modelContext :: ModelContext) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO [LabeledData value model]
fetch !queryBuilderProvider = do
let !(theQuery, theParameters) = queryBuilderProvider
|> toSQL
trackTableRead (tableNameByteString @model)
sqlQuery @_ @(LabeledData value model) (Query $ cs theQuery) theParameters
{-# INLINE fetchOneOrNothing #-}
fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO (Maybe model)
fetchOneOrNothing = commonFetchOneOrNothing
{-# INLINE fetchOne #-}
fetchOne :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO model
fetchOne = commonFetchOne
{-# INLINE commonFetch #-}
commonFetch :: forall model table queryBuilderProvider joinRegister. (Table model, HasQueryBuilder queryBuilderProvider joinRegister, model ~ GetModelByTableName table, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext) => queryBuilderProvider table -> IO [model]
commonFetch !queryBuilder = do
let !(theQuery, theParameters) = queryBuilder
|> toSQL
trackTableRead (tableNameByteString @model)
sqlQuery (Query $ cs theQuery) theParameters
{-# INLINE commonFetchOneOrNothing #-}
commonFetchOneOrNothing :: forall model table queryBuilderProvider joinRegister. (?modelContext :: ModelContext) => (Table model, KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister, PG.FromRow model) => queryBuilderProvider table -> IO (Maybe model)
commonFetchOneOrNothing !queryBuilder = do
let !(theQuery, theParameters) = queryBuilder
|> buildQuery
|> setJust #limitClause "LIMIT 1"
|> toSQL'
trackTableRead (tableNameByteString @model)
results <- sqlQuery (Query $ cs theQuery) theParameters
pure $ listToMaybe results
{-# INLINE commonFetchOne #-}
commonFetchOne :: forall model table queryBuilderProvider joinRegister. (?modelContext :: ModelContext) => (Table model, KnownSymbol table, Fetchable (queryBuilderProvider table) model, HasQueryBuilder queryBuilderProvider joinRegister, PG.FromRow model) => queryBuilderProvider table -> IO model
commonFetchOne !queryBuilder = do
maybeModel <- fetchOneOrNothing queryBuilder
case maybeModel of
Just model -> pure model
Nothing -> throwIO RecordNotFoundException { queryAndParams = toSQL queryBuilder }
-- | Returns the count of records selected by the query builder.
--
-- __Example:__ Counting all users.
--
-- > allUsersCount <- query @User |> fetchCount -- SELECT COUNT(*) FROM users
--
--
-- __Example:__ Counting all active projects
--
-- > activeProjectsCount <- query @Project
-- > |> filterWhere (#isActive, True)
-- > |> fetchCount
-- > -- SELECT COUNT(*) FROM projects WHERE is_active = true
fetchCount :: forall table queryBuilderProvider joinRegister. (?modelContext :: ModelContext, KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister) => queryBuilderProvider table -> IO Int
fetchCount !queryBuilder = do
let !(theQuery', theParameters) = toSQL' (buildQuery queryBuilder)
let theQuery = "SELECT COUNT(*) FROM (" <> theQuery' <> ") AS _count_values"
trackTableRead (symbolToByteString @table)
[PG.Only count] <- sqlQuery (Query $! cs theQuery) theParameters
pure count
{-# INLINE fetchCount #-}
-- | Checks whether the query has any results.
--
-- Returns @True@ when there is at least one row matching the conditions of the query. Returns @False@ otherwise.
--
-- __Example:__ Checking whether there are unread messages
--
-- > hasUnreadMessages <- query @Message
-- > |> filterWhere (#isUnread, True)
-- > |> fetchExists
-- > -- SELECT EXISTS (SELECT * FROM messages WHERE is_unread = true)
fetchExists :: forall table queryBuilderProvider joinRegister. (?modelContext :: ModelContext, KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister) => queryBuilderProvider table -> IO Bool
fetchExists !queryBuilder = do
let !(theQuery', theParameters) = toSQL' (buildQuery queryBuilder)
let theQuery = "SELECT EXISTS (" <> theQuery' <> ") AS _exists_values"
trackTableRead (symbolToByteString @table)
[PG.Only exists] <- sqlQuery (Query $! cs theQuery) theParameters
pure exists
{-# INLINE fetchExists #-}
{-# INLINE genericFetchId #-}
genericFetchId :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, FilterPrimaryKey table, model ~ GetModelByTableName table, GetTableName model ~ table) => Id' table -> IO [model]
genericFetchId !id = query @model |> filterWhereId id |> fetch
{-# INLINE genericfetchIdOneOrNothing #-}
genericfetchIdOneOrNothing :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, FilterPrimaryKey table, model ~ GetModelByTableName table, GetTableName model ~ table) => Id' table -> IO (Maybe model)
genericfetchIdOneOrNothing !id = query @model |> filterWhereId id |> fetchOneOrNothing
{-# INLINE genericFetchIdOne #-}
genericFetchIdOne :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, FilterPrimaryKey table, model ~ GetModelByTableName table, GetTableName model ~ table) => Id' table -> IO model
genericFetchIdOne !id = query @model |> filterWhereId id |> fetchOne
{-# INLINE genericFetchIds #-}
genericFetchIds :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, model ~ GetModelByTableName table, GetTableName model ~ table) => [Id model] -> IO [model]
genericFetchIds !ids = query @model |> filterWhereIdIn ids |> fetch
{-# INLINE genericfetchIdsOneOrNothing #-}
genericfetchIdsOneOrNothing :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, model ~ GetModelByTableName table, GetTableName model ~ table) => [Id model] -> IO (Maybe model)
genericfetchIdsOneOrNothing !ids = query @model |> filterWhereIdIn ids |> fetchOneOrNothing
{-# INLINE genericFetchIdsOne #-}
genericFetchIdsOne :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, model ~ GetModelByTableName table, GetTableName model ~ table) => [Id model] -> IO model
genericFetchIdsOne !ids = query @model |> filterWhereIdIn ids |> fetchOne
{-# INLINE findBy #-}
findBy !field !value !queryBuilder = queryBuilder |> filterWhere (field, value) |> fetchOne
{-# INLINE findMaybeBy #-}
findMaybeBy !field !value !queryBuilder = queryBuilder |> filterWhere (field, value) |> fetchOneOrNothing
--findManyBy :: (?modelContext :: ModelContext, PG.FromRow model, KnownSymbol name, ToField value, HasField name value model) => Proxy name -> value -> QueryBuilder model -> IO [model]
{-# INLINE findManyBy #-}
findManyBy !field !value !queryBuilder = queryBuilder |> filterWhere (field, value) |> fetch
-- Step.findOneByWorkflowId id == queryBuilder |> findBy #templateId id
instance (model ~ GetModelById (Id' table), GetTableName model ~ table, FilterPrimaryKey table) => Fetchable (Id' table) model where
type FetchResult (Id' table) model = model
{-# INLINE fetch #-}
fetch = genericFetchIdOne
{-# INLINE fetchOneOrNothing #-}
fetchOneOrNothing = genericfetchIdOneOrNothing
{-# INLINE fetchOne #-}
fetchOne = genericFetchIdOne
instance (model ~ GetModelById (Id' table), GetTableName model ~ table, FilterPrimaryKey table) => Fetchable (Maybe (Id' table)) model where
type FetchResult (Maybe (Id' table)) model = [model]
{-# INLINE fetch #-}
fetch (Just a) = genericFetchId a
fetch Nothing = pure []
{-# INLINE fetchOneOrNothing #-}
fetchOneOrNothing Nothing = pure Nothing
fetchOneOrNothing (Just a) = genericfetchIdOneOrNothing a
{-# INLINE fetchOne #-}
fetchOne (Just a) = genericFetchIdOne a
fetchOne Nothing = error "Fetchable (Maybe Id): Failed to fetch because given id is 'Nothing', 'Just id' was expected"
instance (model ~ GetModelById (Id' table), GetModelByTableName table ~ model, GetTableName model ~ table) => Fetchable [Id' table] model where
type FetchResult [Id' table] model = [model]
{-# INLINE fetch #-}
fetch = genericFetchIds
{-# INLINE fetchOneOrNothing #-}
fetchOneOrNothing = genericfetchIdsOneOrNothing
{-# INLINE fetchOne #-}
fetchOne = genericFetchIdsOne
fetchSQLQuery :: (PG.FromRow model, ?modelContext :: ModelContext) => SQLQuery -> IO [model]
fetchSQLQuery theQuery = do
let (sql, theParameters) = toSQL' theQuery
trackTableRead (theQuery.selectFrom)
sqlQuery (Query $ cs sql) theParameters
-- | Returns the latest record or Nothing
--
-- __Example:__
--
-- > latestUser <-
-- > query @User
-- > |> fetchLatest
-- >
--
-- 'fetchLatest' is mainly a shortcut for code like this:
--
-- > latestUser <-
-- > query @User
-- > |> orderByDesc #createdAt
-- > |> fetchOneOrNothing
--
fetchLatest :: forall table queryBuilderProvider joinRegister model.
( ?modelContext :: ModelContext
, model ~ GetModelByTableName table
, KnownSymbol table
, HasQueryBuilder queryBuilderProvider joinRegister
, HasField "createdAt" model UTCTime
, Fetchable (queryBuilderProvider table) model
, Table model
, FromRow model
) => queryBuilderProvider table -> IO (Maybe model)
fetchLatest queryBuilder = queryBuilder |> fetchLatestBy #createdAt
-- | Provided a field name, it returns the latest record or Nothing
--
-- See 'fetchLatest' if you're looking for the latest record by the createdAt timestamp.
--
-- __Example:__
--
-- > latestTrialUser <-
-- > query @User
-- > |> fetchLatestBy #trialStartedAt
-- >
--
-- 'fetchLatestBy' is mainly a shortcut for code like this:
--
-- > latestUser <-
-- > query @User
-- > |> orderByDesc #trialStartedAt
-- > |> fetchOneOrNothing
--
fetchLatestBy :: forall table createdAt queryBuilderProvider joinRegister model.
( ?modelContext :: ModelContext
, KnownSymbol createdAt
, model ~ GetModelByTableName table
, KnownSymbol table
, HasQueryBuilder queryBuilderProvider joinRegister
, HasField createdAt model UTCTime
, Fetchable (queryBuilderProvider table) model
, Table model
, FromRow model
) => Proxy createdAt -> queryBuilderProvider table -> IO (Maybe model)
fetchLatestBy field queryBuilder =
queryBuilder
|> orderByDesc field
|> fetchOneOrNothing