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