rel8-1.7.0.0: src/Rel8/Statement/OnConflict.hs
{-# language DuplicateRecordFields #-}
{-# language FlexibleContexts #-}
{-# language GADTs #-}
{-# language LambdaCase #-}
{-# language NamedFieldPuns #-}
{-# language OverloadedStrings #-}
{-# language RecordWildCards #-}
{-# language StandaloneKindSignatures #-}
{-# language StrictData #-}
{-# language TypeOperators #-}
module Rel8.Statement.OnConflict
( OnConflict(..)
, Upsert(..)
, ppOnConflict
)
where
-- base
import Data.Foldable ( toList )
import Data.Kind ( Type )
import Prelude
-- opaleye
import qualified Opaleye.Internal.HaskellDB.Sql.Print as Opaleye
import qualified Opaleye.Internal.Sql as Opaleye
-- pretty
import Text.PrettyPrint ( Doc, (<+>), ($$), parens, text )
-- rel8
import Rel8.Expr ( Expr )
import Rel8.Expr.Opaleye (toPrimExpr)
import Rel8.Schema.Name ( Name, Selects, ppColumn )
import Rel8.Schema.Table ( TableSchema(..) )
import Rel8.Statement.Set ( ppSet )
import Rel8.Statement.Where ( ppWhere )
import Rel8.Table ( Table, toColumns )
import Rel8.Table.Cols ( Cols( Cols ) )
import Rel8.Table.Name ( showNames )
import Rel8.Table.Opaleye (attributes, view)
import Rel8.Table.Projection ( Projecting, Projection, apply )
-- | 'OnConflict' represents the @ON CONFLICT@ clause of an @INSERT@
-- statement. This specifies what ought to happen when one or more of the
-- rows proposed for insertion conflict with an existing row in the table.
type OnConflict :: Type -> Type
data OnConflict names
= Abort
-- ^ Abort the transaction if there are conflicting rows (Postgres' default)
| DoNothing
-- ^ @ON CONFLICT DO NOTHING@
| DoUpdate (Upsert names)
-- ^ @ON CONFLICT DO UPDATE@
-- | The @ON CONFLICT (...) DO UPDATE@ clause of an @INSERT@ statement, also
-- known as \"upsert\".
--
-- When an existing row conflicts with a row proposed for insertion,
-- @ON CONFLICT DO UPDATE@ allows you to instead update this existing row. The
-- conflicting row proposed for insertion is then \"excluded\", but its values
-- can still be referenced from the @SET@ and @WHERE@ clauses of the @UPDATE@
-- statement.
--
-- Upsert in Postgres a \"conflict target\" to be specified — this is the
-- @UNIQUE@ index from conflicts with which we would like to recover. Indexes
-- are specified by listing the columns that comprise them along with an
-- optional predicate in the case of partial indexes.
type Upsert :: Type -> Type
data Upsert names where
Upsert :: (Selects names exprs, Projecting names index, excluded ~ exprs) =>
{ index :: Projection names index
-- ^ The set of columns comprising the @UNIQUE@ index that forms our
-- conflict target, projected from the set of columns for the whole
-- table
, predicate :: Maybe (exprs -> Expr Bool)
-- ^ An optional predicate used to specify a
-- [partial index](https://www.postgresql.org/docs/current/indexes-partial.html).
, set :: excluded -> exprs -> exprs
-- ^ How to update each selected row.
, updateWhere :: excluded -> exprs -> Expr Bool
-- ^ Which rows to select for update.
}
-> Upsert names
ppOnConflict :: TableSchema names -> OnConflict names -> Doc
ppOnConflict schema = \case
Abort -> mempty
DoNothing -> text "ON CONFLICT DO NOTHING"
DoUpdate upsert -> ppUpsert schema upsert
ppUpsert :: TableSchema names -> Upsert names -> Doc
ppUpsert schema@TableSchema {columns} Upsert {..} =
text "ON CONFLICT" <+>
ppIndex columns index <+> foldMap (ppPredicate columns) predicate <+>
text "DO UPDATE" $$
ppSet schema (set excluded) $$
ppWhere schema (updateWhere excluded)
where
excluded = attributes TableSchema
{ name = "excluded"
, columns
}
ppIndex :: (Table Name names, Projecting names index)
=> names -> Projection names index -> Doc
ppIndex columns index =
parens $ Opaleye.commaV ppColumn $ toList $
showNames $ Cols $ apply index $ toColumns columns
ppPredicate :: Selects names exprs
=> names -> (exprs -> Expr Bool) -> Doc
ppPredicate schema where_ = text "WHERE" <+> ppExpr condition
where
ppExpr = Opaleye.ppSqlExpr . Opaleye.sqlExpr . toPrimExpr
condition = where_ (view schema)