packages feed

poppy-1.0.0: src/Poppy/Internal/SelectIn.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_HADDOCK hide #-}

module Poppy.Internal.SelectIn
  ( GroupIndex,
    ByPk,
    findByIn,
    emptyGroups,
    indexHasMany,
    indexHasManyMaybe,
    lookupGroups,
    emptyByPk,
    indexByPk,
    lookupByPk,
    prepareIncludeRootQuery,
  )
where

import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Database.PostgreSQL.Simple as PGSimple
import Database.PostgreSQL.Simple.FromRow (FromRow, fromRow)
import Database.PostgreSQL.Simple.ToField (Action, ToField, toField)
import Database.PostgreSQL.Simple.Types (In (..), Query (..))
import Poppy.Internal.Core (Entity (..), Field (..))
import Poppy.Internal.Db (Db, dbIO, logSql)
import Poppy.Internal.Group (groupByKey)
import qualified Poppy.Internal.Operations as Ops
import Poppy.Internal.Query
  ( OrderBy (..),
    OrderDirection (..),
    QueryBuilder,
    matching,
    orderBy,
    queryOrderBy,
    setOrderBy,
  )
import Poppy.Internal.Sql (quoteIdent)
import Poppy.Internal.Where (Where, compileWhere, in_)

newtype GroupIndex k a = GroupIndex (Map k [a])

newtype ByPk k a = ByPk (Map k a)

findByIn ::
  forall table result key.
  (Entity table, FromRow result, ToField key) =>
  Field table key ->
  [key] ->
  Maybe (Where table) ->
  [OrderBy table] ->
  Maybe Int ->
  Db [result]
findByIn field keys mWhere orders mTake
  | null keys = pure []
  | Just n <- mTake, n <= 0 = pure []
  | Just n <- mTake = findTaken @table @result field keys mWhere orders n
  | otherwise =
      Ops.findMany @table @result $
        setOrderBy (childOrder @table orders)
          . maybe id matching mWhere
          . matching (in_ field keys)

findTaken ::
  forall table result key.
  (Entity table, FromRow result, ToField key) =>
  Field table key ->
  [key] ->
  Maybe (Where table) ->
  [OrderBy table] ->
  Int ->
  Db [result]
findTaken field keys mWhere orders n = do
  let (sql, params) = takenSql @table field keys mWhere orders n
  logSql sql
  dbIO $ \conn -> PGSimple.queryWith fromRow conn (Query (TE.encodeUtf8 sql)) params

takenSql ::
  forall table key.
  (Entity table, ToField key) =>
  Field table key ->
  [key] ->
  Maybe (Where table) ->
  [OrderBy table] ->
  Int ->
  (Text, [Action])
takenSql field keys mWhere orders n =
  let cols = T.intercalate ", " (map quoteIdent (tableColumns @table))
      part = quoteIdent (fieldColumn field)
      rn = quoteIdent "poppy_rn"
      (extraSql, extraParams) = case mWhere of
        Nothing -> ("", [])
        Just pred_ ->
          let (clause, clauseParams) = compileWhere pred_
           in (" AND (" <> clause <> ")", clauseParams)
      sql =
        "SELECT "
          <> cols
          <> " FROM (SELECT "
          <> cols
          <> ", ROW_NUMBER() OVER (PARTITION BY "
          <> part
          <> " ORDER BY "
          <> orderSql (childOrder @table orders)
          <> ") AS "
          <> rn
          <> " FROM "
          <> quoteIdent (tableName @table)
          <> " WHERE "
          <> part
          <> " IN ?"
          <> extraSql
          <> ") AS "
          <> quoteIdent "poppy_window"
          <> " WHERE "
          <> rn
          <> " <= ? ORDER BY "
          <> part
          <> " ASC, "
          <> rn
          <> " ASC"
   in (sql, toField (In keys) : extraParams ++ [toField n])

childOrder :: forall table. (Entity table) => [OrderBy table] -> [OrderBy table]
childOrder orders
  | null orders = [OrderBy pk Asc]
  | any isPk orders = orders
  | otherwise = orders ++ [OrderBy pk Asc]
  where
    pk = fieldColumn (primaryKey @table)
    isPk (OrderBy col _) = col == pk

orderSql :: [OrderBy table] -> Text
orderSql orders = T.intercalate ", " (map term orders)
  where
    term (OrderBy col dir) =
      quoteIdent col <> case dir of
        Asc -> " ASC"
        Desc -> " DESC"

emptyGroups :: GroupIndex k a
emptyGroups = GroupIndex Map.empty

indexHasMany :: (Ord k) => (a -> k) -> [a] -> GroupIndex k a
indexHasMany keyFn rows =
  GroupIndex . Map.fromList $
    [(keyFn (head group), group) | group <- groupByKey keyFn rows]

-- | Like 'indexHasMany', dropping rows whose key is 'Nothing'.
indexHasManyMaybe :: (Ord k) => (a -> Maybe k) -> [a] -> GroupIndex k a
indexHasManyMaybe keyFn rows =
  GroupIndex $
    Map.fromListWith (flip (<>)) [(key, [row]) | row <- rows, Just key <- [keyFn row]]

lookupGroups :: (Ord k) => k -> GroupIndex k a -> [a]
lookupGroups key (GroupIndex groups) =
  Map.findWithDefault [] key groups

emptyByPk :: ByPk k a
emptyByPk = ByPk Map.empty

indexByPk :: (Ord k) => (a -> k) -> [a] -> ByPk k a
indexByPk keyFn rows =
  ByPk (Map.fromList [(keyFn row, row) | row <- rows])

lookupByPk :: (Ord k) => k -> ByPk k a -> Maybe a
lookupByPk key (ByPk byPk) =
  Map.lookup key byPk

prepareIncludeRootQuery ::
  forall table.
  (Entity table) =>
  (QueryBuilder table -> QueryBuilder table) ->
  QueryBuilder table ->
  QueryBuilder table
prepareIncludeRootQuery modifier =
  defaultPkOrder @table . modifier

defaultPkOrder ::
  forall table.
  (Entity table) =>
  QueryBuilder table ->
  QueryBuilder table
defaultPkOrder qb =
  case queryOrderBy qb of
    [] -> orderBy (primaryKey @table) Asc qb
    _ -> qb