packages feed

rel8-1.5.0.0: src/Rel8/Statement.hs

{-# language DeriveFunctor #-}
{-# language DerivingVia #-}
{-# language FlexibleContexts #-}
{-# language GADTs #-}
{-# language LambdaCase #-}
{-# language NamedFieldPuns #-}
{-# language RankNTypes #-}
{-# language RecordWildCards #-}
{-# language ScopedTypeVariables #-}
{-# language StandaloneKindSignatures #-}
{-# language TypeApplications #-}

module Rel8.Statement
  ( Statement
  , statementReturning
  , statementNoReturning
  , ppDecodeStatement
  )
where

-- base
import Control.Applicative (liftA2)
import Control.Monad (ap, liftM2)
import Data.Foldable (fold, toList)
import Data.Int (Int64)
import Data.Kind (Type)
import Data.List.NonEmpty (NonEmpty, intersperse)
import Data.Monoid (Endo (Endo))
import Data.String (fromString)
import Prelude

-- hasql
import qualified Hasql.Decoders as Hasql

-- opaleye
import qualified Opaleye.Internal.Tag as Opaleye

-- pretty
import Text.PrettyPrint
  ( Doc
  , (<+>)
  , ($$)
  , comma
  , hcat
  , parens
  , punctuate
  , text
  , vcat
  )

-- rel8
import Rel8.Expr (Expr)
import Rel8.Expr.Bool (false)
import Rel8.Query (Query)
import Rel8.Query.Aggregate (countRows)
import Rel8.Query.Each (each)
import Rel8.Schema.Escape (escape)
import Rel8.Schema.Table (TableSchema (..))
import Rel8.Statement.Rows (Rows (..))
import Rel8.Table (Table)
import Rel8.Table.Cols (fromCols)
import Rel8.Table.Name (namesFromLabelsWithA, showNames)
import Rel8.Table.Serialize (parse)

-- semigroupoids
import Data.Functor.Apply (Apply, WrappedApplicative (..))
import Data.Functor.Bind (Bind, (>>-))

-- transformers
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.State.Strict (State, evalState)
import Control.Monad.Trans.Writer.CPS (WriterT, runWriterT, tell)


type Binding :: Type
data Binding = Binding
  { relation :: !String
  , columns :: !(Maybe (NonEmpty String))
  , doc :: !Doc
  , returning :: !Returning
  }


type Result :: Type -> Type
data Result a = Unmodified !a | Modified !a


instance Functor Result where
  fmap f = \case
    Unmodified a -> Modified (f a)
    Modified a -> Modified (f a)


getResult :: Result a -> a
getResult = \case
  Unmodified a -> a
  Modified a -> a


type Returning :: Type
data Returning where
  NoReturning :: Returning
  Returning :: Query (Expr Int64) -> Returning 


-- | 'Statement' represents a single PostgreSQL statement. Most commonly,
-- this is constructed using 'Rel8.select', 'Rel8.insert', 'Rel8.update'
-- or 'Rel8.delete'.
--
-- However, in addition to @SELECT@, @INSERT@, @UPDATE@ and @DELETE@,
-- PostgreSQL also supports compositions thereof via its statement-level
-- @WITH@ syntax (with some caveats). Each such \"sub-statement\" can
-- reference the results of previous sub-statements. 'Statement' provides a
-- 'Monad' instance that captures this \"binding\" pattern.
--
-- The caveat with this is that the [side-effects of these sub-statements
-- are not visible to other sub-statements](https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-MODIFYING);
-- only the explicit results of previous sub-statements (from @SELECT@s or
-- @RETURNING@ clauses) are visible. So, for example, an @INSERT@ into a table
-- followed immediately by a @SELECT@ therefrom will not return the inserted
-- rows. However, it is possible to return the inserted rows using
-- @RETURNING@, 'Rel8.unionAll'ing this with the result of a @SELECT@
-- from the same table will produce the desired result.
--
-- An example of where this can be useful is if you want to delete rows from
-- a table and simultaneously log their deletion in a log table.
--
-- @
-- deleteFoo :: (Foo Expr -> Expr Bool) -> Statement ()
-- deleteFoo predicate = do
--   foos <-
--     delete Delete
--       { from = fooSchema
--       , using = pure ()
--       , deleteWhere = \_ -> predicate
--       , returning = Returning id
--       }
--   insert Insert
--     { into = deletedFooSchema
--     , rows = do
--         Foo {..} <- foos
--         let
--           deletedAt = 'Rel8.Expr.Time.now'
--         pure DeletedFoo {..}
--     , onConflict = Abort
--     , returning = NoReturning
--     }
-- @
newtype Statement a =
  Statement (WriterT (Endo [Binding]) (State Opaleye.Tag) (Result a))
  deriving stock (Functor)
  deriving (Apply) via WrappedApplicative Statement


instance Applicative Statement where
  pure = Statement . pure . Modified
  (<*>) = ap
  liftA2 = liftM2


instance Bind Statement where
  Statement m >>- f = Statement $ do
    result <- m
    case f (getResult result) of
      Statement m' -> m'


instance Monad Statement where
  (>>=) = (>>-)


statementNoReturning :: State Opaleye.Tag Doc -> Statement ()
statementNoReturning pp = Statement $ do
  binding <- lift $ do
    doc <- pp
    tag <- Opaleye.fresh
    let
      relation = Opaleye.tagWith tag "statement"
      columns = Nothing
      returning = NoReturning
      binding = Binding {..}
    pure binding
  tell (Endo (binding :))
  pure $ Unmodified ()


statementReturning :: Table Expr a 
  => State Opaleye.Tag Doc -> Statement (Query a)
statementReturning pp = Statement $ do
  (binding, query) <- lift $ do
    doc <- pp
    tag <- Opaleye.fresh
    let
      relation = Opaleye.tagWith tag "statement"
      symbol labels = do
        subtag <- Opaleye.fresh
        let
          suffix = Opaleye.tagWith tag (Opaleye.tagWith subtag "")
        pure $ take (63 - length suffix) label ++ suffix
        where
          label = fold (intersperse "/" labels)
      names = namesFromLabelsWithA symbol `evalState` Opaleye.start
      columns = Just $ showNames names
      query =
        fromCols <$> each
          TableSchema
            { name = fromString relation
            , columns = names
            }
      returning = Returning (countRows query)
      binding = Binding {..}
    pure (binding, query)
  tell (Endo (binding :))
  pure $ Unmodified query


ppDecodeStatement :: ()
  => (forall x. Table Expr x => Query x -> State Opaleye.Tag Doc)
  -> Rows exprs a -> Statement exprs -> (Doc, Hasql.Result a)
ppDecodeStatement ppSelect rows (Statement m) = evalState go Opaleye.start
  where
    go = do
      (result, Endo dlist) <- runWriterT m
      let
        bindings' = dlist []
      case unsnoc bindings' of
        Nothing -> case rows of
          Void -> do
            doc <- ppSelect (pure false)
            pure (doc, Hasql.noResult)
          RowsAffected -> do
            doc <- ppSelect (pure false)
            pure (doc, 0 <$ Hasql.noResult)
          Single @exprs @a -> do
            doc <- ppSelect (getResult result)
            pure (doc, Hasql.singleRow (parse @exprs @a))
          Maybe @exprs @a -> do
            doc <- ppSelect (getResult result)
            pure (doc, Hasql.rowMaybe (parse @exprs @a))
          List @exprs @a -> do
            doc <- ppSelect (getResult result)
            pure (doc, Hasql.rowList (parse @exprs @a))
          Vector @exprs @a -> do
            doc <- ppSelect (getResult result)
            pure (doc, Hasql.rowVector (parse @exprs @a))
        Just (bindings, binding@Binding {doc = after}) -> case rows of
          Void -> pure (doc, Hasql.noResult)
            where
              doc = ppWith bindings after
          RowsAffected -> do
            case result of
              Unmodified _ -> pure (doc, Hasql.rowsAffected)
                where
                  doc = ppWith bindings after
              Modified _ -> case returning binding of
                NoReturning -> pure (doc, Hasql.rowsAffected)
                  where
                    doc = ppWith bindings after 
                Returning query -> do
                  doc <- ppWith bindings' <$> ppSelect query
                  pure (doc, Hasql.singleRow parse)
          Single @exprs @a -> do
            case result of
              Unmodified _ -> pure (doc, Hasql.singleRow (parse @exprs @a))
                where
                  doc = ppWith bindings after
              Modified query -> do
                doc <- ppWith bindings' <$> ppSelect query
                pure (doc, Hasql.singleRow (parse @exprs @a))
          Maybe @exprs @a -> do
            case result of
              Unmodified _ -> pure (doc, Hasql.rowMaybe (parse @exprs @a))
                where
                  doc = ppWith bindings after
              Modified query -> do
                doc <- ppWith bindings' <$> ppSelect query
                pure (doc, Hasql.rowMaybe (parse @exprs @a))
          List @exprs @a -> do
            case result of
              Unmodified _ -> pure (doc, Hasql.rowList (parse @exprs @a))
                where
                  doc = ppWith bindings after
              Modified query -> do
                doc <- ppWith bindings' <$> ppSelect query
                pure (doc, Hasql.rowList (parse @exprs @a))
          Vector @exprs @a -> do
            case result of
              Unmodified _ -> pure (doc, Hasql.rowVector (parse @exprs @a))
                where
                  doc = ppWith bindings after
              Modified query -> do
                doc <- ppWith bindings' <$> ppSelect query
                pure (doc, Hasql.rowVector (parse @exprs @a))


ppWith :: [Binding] -> Doc -> Doc
ppWith bindings after = pre $$ after
  where
    pre = case bindings of
      [] -> mempty
      _ ->
        text "WITH" <+>
        vcat (punctuate comma (map go bindings))
    go binding@Binding {doc = before} =
      ppAlias binding $$
      text "AS" <+>
      parens before


ppAlias :: Binding -> Doc
ppAlias Binding {relation, columns = mcolumns} = case mcolumns of
  Nothing -> escape relation
  Just columns -> 
    escape relation <+>
    parens (hcat (punctuate comma (escape <$> toList columns)))


unsnoc :: [a] -> Maybe ([a], a)
unsnoc = foldr (\x -> Just . maybe ([], x) (\(~(a, b)) -> (x : a, b))) Nothing