packages feed

orville-postgresql-1.1.0.0: src/Orville/PostgreSQL/Expr/OnConflict.hs

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

{- |
Copyright : Flipstone Technology Partners 2024
License   : MIT
Stability : Stable

@since 1.1.0.0
-}
module Orville.PostgreSQL.Expr.OnConflict
  ( OnConflictExpr
  , onConflict
  , onConflictDoNothing
  , onConflictDoUpdate
  , ConflictTargetExpr
  , conflictTargetForIndexColumn
  , conflictTargetForIndexExpr
  , conflictTargetForConstraint
  , ConflictActionExpr
  , ConflictSetItemExpr
  , setColumnNameExcluded
  )
where

import Data.List.NonEmpty (NonEmpty)

import Orville.PostgreSQL.Expr.Index (IndexBodyExpr)
import Orville.PostgreSQL.Expr.Internal.Name.ConstraintName (ConstraintName)
import Orville.PostgreSQL.Expr.Name (ColumnName, QualifiedOrUnqualified)
import Orville.PostgreSQL.Expr.WhereClause (WhereClause)
import qualified Orville.PostgreSQL.Raw.RawSql as RawSql

{- | Type to represent the SQL for the 'ON CONFLICT' clause.

'OnConflict' provides a 'RawSql.SqlExpression' instance. See 'RawSql.unsafeSqlExpression' for how to
construct a value with your own custom SQL.

@since 1.1.0.0
-}
newtype OnConflictExpr
  = OnConflictExpr RawSql.RawSql
  deriving
    ( -- | @since 1.1.0.0
      RawSql.SqlExpression
    )

{- | Create an 'OnConflict' that performs the specified action to be taken during a conflicting insert.

@since 1.1.0.0
-}
onConflict :: Maybe ConflictTargetExpr -> ConflictActionExpr -> OnConflictExpr
onConflict mbTarget action =
  OnConflictExpr $
    RawSql.fromString "ON CONFLICT "
      <> case mbTarget of
        Nothing ->
          RawSql.toRawSql action
        Just target ->
          RawSql.toRawSql target <> RawSql.space <> RawSql.toRawSql action

{- | A wrapper around 'onConflict' for no specific target and an explicit no action to perform.

@since 1.1.0.0
-}
onConflictDoNothing :: OnConflictExpr
onConflictDoNothing =
  onConflict Nothing doNothingActionExpr

{- | A wrapper around 'onConflict' to perform an update, with optional target and where clause. That
   will use the given 'ConflictSetItemExpr's as the update body.

@since 1.1.0.0
-}
onConflictDoUpdate :: Maybe ConflictTargetExpr -> NonEmpty ConflictSetItemExpr -> Maybe WhereClause -> OnConflictExpr
onConflictDoUpdate mbTarget setItems =
  onConflict mbTarget . doUpdateSetActionExpr setItems

{- | Type to represent the target portion of the SQL 'ON CONFLICT target action'

@since 1.1.0.0
-}
newtype ConflictTargetExpr
  = ConflictTargetExpr RawSql.RawSql
  deriving
    ( -- | @since 1.1.0.0
      RawSql.SqlExpression
    )

{- | Build a 'ConflictTargetExpr' that would conflict on a single 'ColumnName'. PostgreSQL will use
   the column to infer which index to check for conflicts.

@since 1.1.0.0
-}
conflictTargetForIndexColumn :: QualifiedOrUnqualified ColumnName -> Maybe WhereClause -> ConflictTargetExpr
conflictTargetForIndexColumn colName mbWhere =
  let
    parensCol = RawSql.parenthesized $ RawSql.toRawSql colName
  in
    ConflictTargetExpr $
      case mbWhere of
        Nothing -> parensCol
        Just whereClause -> parensCol <> RawSql.space <> RawSql.toRawSql whereClause

{- | Build a 'ConflictTargetExpr' that would conflict on an 'IndexBodyExpr'. This can be used to check
   for more than a single column. PostgreSQL will use the expression to infer which index to check for
   conflicts.

   Note that this function assumes that the 'IndexBodyExpr' is parenthesized.
@since 1.1.0.0
-}
conflictTargetForIndexExpr :: IndexBodyExpr -> Maybe WhereClause -> ConflictTargetExpr
conflictTargetForIndexExpr idxBody mbWhere =
  let
    rawIdxBody = RawSql.toRawSql idxBody
  in
    ConflictTargetExpr $
      case mbWhere of
        Nothing -> rawIdxBody
        Just whereClause -> rawIdxBody <> RawSql.space <> RawSql.toRawSql whereClause

{- | Build a 'ConflictTargetExpr' for conflicting on the given 'ConstraintName'. For conflicing on an
   index, see 'conflictTargetForIndexColumn' or 'conflictTargetForIndexExpr' instead.

@since 1.1.0.0
-}
conflictTargetForConstraint :: ConstraintName -> ConflictTargetExpr
conflictTargetForConstraint constraintName =
  ConflictTargetExpr $ RawSql.fromString "ON CONSTRAINT " <> RawSql.toRawSql constraintName

{- | Type to represent the action portion of the SQL 'ON CONFLICT target action'

@since 1.1.0.0
-}
newtype ConflictActionExpr
  = ConflictActionExpr RawSql.RawSql
  deriving
    ( -- | @since 1.1.0.0
      RawSql.SqlExpression
    )

{- | A 'ConflictActionExpr' for the SQL 'DO NOTHING'.

@since 1.1.0.0
-}
doNothingActionExpr :: ConflictActionExpr
doNothingActionExpr =
  ConflictActionExpr $ RawSql.fromString "DO NOTHING"

{- | A 'ConflictActionExpr' for the SQL 'DO UPDATE SET', that will use the given 'ConflictSetItemExpr'
   to determine which columns to update and the optional 'WhereClause' to peform filtering.

@since 1.1.0.0
-}
doUpdateSetActionExpr :: NonEmpty ConflictSetItemExpr -> Maybe WhereClause -> ConflictActionExpr
doUpdateSetActionExpr setItems mbWhere =
  let
    commaSeparatedItems = RawSql.intercalate RawSql.comma setItems
  in
    ConflictActionExpr $
      RawSql.fromString "DO UPDATE SET "
        <> case mbWhere of
          Nothing -> commaSeparatedItems
          Just whereClause -> commaSeparatedItems <> RawSql.space <> RawSql.toRawSql whereClause

{- | Type to represent the item to be set in an 'ON CONFLICT DO UPDATE'. E.G.

> ON CONFLICT DO UPDATE SET item

@since 1.1.0.0
-}
newtype ConflictSetItemExpr
  = ConflictSetItemExpr RawSql.RawSql
  deriving
    ( -- | @since 1.1.0.0
      RawSql.SqlExpression
    )

{- | Builds a 'ConflictSetItemExpr' that will set the given 'ColumnName' to the excluded value using
the special pseudo-table 'EXCLUDED'.

@since 1.1.0.0
-}
setColumnNameExcluded :: QualifiedOrUnqualified ColumnName -> ConflictSetItemExpr
setColumnNameExcluded colName =
  let
    rawName = RawSql.toRawSql colName
  in
    ConflictSetItemExpr $
      rawName
        <> RawSql.fromString " = "
        <> RawSql.fromString "EXCLUDED"
        <> RawSql.dot
        <> rawName