packages feed

rel8-1.1.0.0: src/Rel8/Statement/Returning.hs

{-# language GADTs #-}
{-# language LambdaCase #-}
{-# language NamedFieldPuns #-}
{-# language RankNTypes #-}
{-# language ScopedTypeVariables #-}
{-# language StandaloneKindSignatures #-}
{-# language StrictData #-}
{-# language TypeApplications #-}

module Rel8.Statement.Returning
  ( Returning( NumberOfRowsAffected, Projection )
  , decodeReturning
  , ppReturning
  )
where

-- base
import Control.Applicative ( liftA2 )
import Data.Foldable ( toList )
import Data.Int ( Int64 )
import Data.Kind ( Type )
import Data.List.NonEmpty ( NonEmpty )
import Prelude

-- hasql
import qualified Hasql.Decoders as Hasql

-- opaleye
import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
import qualified Opaleye.Internal.HaskellDB.Sql.Print as Opaleye
import qualified Opaleye.Internal.Sql as Opaleye

-- pretty
import Text.PrettyPrint ( Doc, (<+>), text )

-- rel8
import Rel8.Schema.Name ( Selects )
import Rel8.Schema.Table ( TableSchema(..) )
import Rel8.Table.Opaleye ( castTable, exprs, view )
import Rel8.Table.Serialize ( Serializable, parse )

-- semigropuoids
import Data.Functor.Apply ( Apply, (<.>) )


-- | 'Rel8.Insert', 'Rel8.Update' and 'Rel8.Delete' all support returning either
-- the number of rows affected, or the actual rows modified.
type Returning :: Type -> Type -> Type
data Returning names a where
  Pure :: a -> Returning names a
  Ap :: Returning names (a -> b) -> Returning names a -> Returning names b

  -- | Return the number of rows affected.
  NumberOfRowsAffected :: Returning names Int64

  -- | 'Projection' allows you to project out of the affected rows, which can
  -- be useful if you want to log exactly which rows were deleted, or to view
  -- a generated id (for example, if using a column with an autoincrementing
  -- counter via 'Rel8.nextval').
  Projection :: (Selects names exprs, Serializable returning a)
    => (exprs -> returning)
    -> Returning names [a]


instance Functor (Returning names) where
  fmap f = \case
    Pure a -> Pure (f a)
    Ap g a -> Ap (fmap (f .) g) a
    m -> Ap (Pure f) m


instance Apply (Returning names) where
  (<.>) = Ap


instance Applicative (Returning names) where
  pure = Pure
  (<*>) = Ap


projections :: ()
  => TableSchema names -> Returning names a -> Maybe (NonEmpty Opaleye.PrimExpr)
projections schema@TableSchema {columns} = \case
  Pure _ -> Nothing
  Ap f a -> projections schema f <> projections schema a
  NumberOfRowsAffected -> Nothing
  Projection f -> Just (exprs (castTable (f (view columns))))


runReturning :: ()
  => ((Int64 -> a) -> r)
  -> (forall x. Hasql.Row x -> ([x] -> a) -> r)
  -> Returning names a
  -> r
runReturning rowCount rowList = \case
  Pure a -> rowCount (const a)
  Ap fs as ->
    runReturning
      (\withCount ->
         runReturning
           (\withCount' -> rowCount (withCount <*> withCount'))
           (\decoder -> rowList decoder . liftA2 withCount length64)
           as)
      (\decoder withRows ->
         runReturning
           (\withCount -> rowList decoder $ withRows <*> withCount . length64)
           (\decoder' withRows' ->
             rowList (liftA2 (,) decoder decoder') $
               withRows <$> fmap fst <*> withRows' . fmap snd)
           as)
      fs
  NumberOfRowsAffected -> rowCount id
  Projection (_ :: exprs -> returning) -> rowList decoder' id
    where
      decoder' = parse @returning
  where
    length64 :: Foldable f => f x -> Int64
    length64 = fromIntegral . length


decodeReturning :: Returning names a -> Hasql.Result a
decodeReturning = runReturning
  (<$> Hasql.rowsAffected)
  (\decoder withRows -> withRows <$> Hasql.rowList decoder)


ppReturning :: TableSchema names -> Returning names a -> Doc
ppReturning schema returning = case projections schema returning of
  Nothing -> mempty
  Just columns ->
    text "RETURNING" <+> Opaleye.commaV Opaleye.ppSqlExpr (toList sqlExprs)
    where
      sqlExprs = Opaleye.sqlExpr <$> columns