packages feed

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

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

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

@since 1.1.0.0
-}
module Orville.PostgreSQL.Expr.Join
  ( JoinType
  , innerJoinType
  , leftJoinType
  , rightJoinType
  , fullJoinType
  , leftLateralJoinType
  , innerLateralJoinType
  , JoinConstraint
  , joinOnConstraint
  , joinedTable
  , join
  , joining
  ) where

import Orville.PostgreSQL.Expr.TableReferenceList (TableReference)
import Orville.PostgreSQL.Expr.WhereClause (BooleanExpr)
import qualified Orville.PostgreSQL.Raw.RawSql as RawSql

{- | Representation of what kind of 'JOIN' to perform.

  From the documentation at https://www.postgresql.org/docs/15/sql-select.html there are 4 basic
  types of join.

  - Inner join
    Use 'innerJoinType' for constructing
  - Left join
    Sometimes called a left "outer" join. Use 'leftJoinType' for construction.
  - Right join
    Sometimes called a right "outer" join. use 'rightJoinType' for construction.
  - Full join
    Sometimes called a full "outer" join. Use 'fullJoinType' for construction.

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

{- | Constructs a 'JoinType' for an INNER JOIN.

@since 1.1.0.0
-}
innerJoinType :: JoinType
innerJoinType = JoinType $ RawSql.fromString "INNER JOIN"

{- | Constructs a 'JoinType' for a LEFT JOIN.

  Note that the documentation at https://www.postgresql.org/docs/15/sql-select.html describes the
  keyword OUTER as optional, and the sql generated from here omits it for brevity of query.

@since 1.1.0.0
-}
leftJoinType :: JoinType
leftJoinType = JoinType $ RawSql.fromString "LEFT JOIN"

{- | Constructs a 'JoinType' for a RIGHT JOIN.

  Note that the documentation at https://www.postgresql.org/docs/15/sql-select.html describes the
  keyword OUTER as optional, and the sql generated from here omits it for brevity of query.

@since 1.1.0.0
-}
rightJoinType :: JoinType
rightJoinType = JoinType $ RawSql.fromString "RIGHT JOIN"

{- | Constructs a 'JoinType' for a FULL JOIN.

  Note that the documentation at https://www.postgresql.org/docs/15/sql-select.html describes the
  keyword OUTER as optional, and the sql generated from here omits it for brevity of query.

@since 1.1.0.0
-}
fullJoinType :: JoinType
fullJoinType = JoinType $ RawSql.fromString "FULL JOIN"

{- | Constructs a 'JoinType' for a LEFT JOIN LATERAL.

  Note that the documentation at https://www.postgresql.org/docs/15/sql-select.html describes the
  keyword OUTER as optional, and the sql generated from here omits it for brevity of query.

@since 1.1.0.0
-}
leftLateralJoinType :: JoinType
leftLateralJoinType = JoinType $ RawSql.fromString "LEFT JOIN LATERAL"

{- | Constructs a 'JoinType' for an INNER JOIN.

@since 1.1.0.0
-}
innerLateralJoinType :: JoinType
innerLateralJoinType = JoinType $ RawSql.fromString "JOIN LATERAL"

{- | Representation of the "ON" part of a JOIN in sql.

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

{- | Constructs a 'JoinConstraint' from a given 'BooleanExpr' that specifies which rows in the JOIN are
   considering to match.

@since 1.1.0.0
-}
joinOnConstraint :: BooleanExpr -> JoinConstraint
joinOnConstraint booleanExpr =
  JoinConstraint $
    RawSql.fromString "ON " <> RawSql.toRawSql booleanExpr

{- | Constructs a 'TableReference' by creating a join expression from two existing table references.
  The result is an @n+m@ way join, where @n@ is the number of tables referenced in the first table
  reference expression (which itself may be a join) and @m@ is the number of tables referenced by
  the second table reference expression.

  See also: 'join', 'joining'

@since 1.1.0.0
-}
joinedTable ::
  TableReference ->
  JoinType ->
  TableReference ->
  JoinConstraint ->
  TableReference
joinedTable tableRefA joinType tableRefB joinOn =
  RawSql.unsafeFromRawSql $
    RawSql.toRawSql tableRefA
      <> RawSql.space
      <> RawSql.toRawSql joinType
      <> RawSql.space
      <> RawSql.toRawSql tableRefB
      <> RawSql.space
      <> RawSql.toRawSql joinOn

{- | A flipped version of 'joinedTable' that allows joined tables to be constructed in a syntax more
  similar to SQL. For example:

  @@
    fooTableRef
      & join leftJoinType barTableRef someJoinCondition
      & join leftJoinType bazTableRef someOtherJoinCondition
  @@

  See also: 'joining', 'joinedTable'

  @since 1.1.0.0
-}
join ::
  JoinType ->
  TableReference ->
  JoinConstraint ->
  TableReference ->
  TableReference
join joinType tableRefB joinOn tableRefA =
  joinedTable tableRefA joinType tableRefB joinOn

{- | A convenience function for constructing joins by tracking the tables to be joined in a list
  instead of using the '(&)' operator. The tables will be joined in a left associative manner,
  matching the associativity of the analogous SQL expression.

  @@
    joining fooTableRef
      [ join leftJoinType barTableRef someJoinCondition
      , join leftJoinType bazTableRef someOtherJoinCondition
      ]
  @@

  See also: 'join', 'joinedTable'

  @since 1.1.0.0
-}
joining :: TableReference -> [TableReference -> TableReference] -> TableReference
joining tableRef joinList =
  foldr (flip (.)) id joinList tableRef