{-| Module: Squeal.PostgreSQL.Query.From.Join Description: Squeal joins Copyright: (c) Eitan Chatav, 2019 Maintainer: eitan@morphism.tech Stability: experimental Squeal joins -} {-# LANGUAGE ConstraintKinds , DeriveGeneric , DerivingStrategies , FlexibleContexts , FlexibleInstances , GADTs , GeneralizedNewtypeDeriving , LambdaCase , MultiParamTypeClasses , OverloadedLabels , OverloadedStrings , QuantifiedConstraints , ScopedTypeVariables , StandaloneDeriving , TypeApplications , TypeFamilies , TypeInType , TypeOperators , RankNTypes , UndecidableInstances #-} module Squeal.PostgreSQL.Query.From.Join ( -- * Join JoinItem (..) , cross, crossJoin, crossJoinLateral , inner, innerJoin, innerJoinLateral , leftOuter, leftOuterJoin, leftOuterJoinLateral , rightOuter, rightOuterJoin, rightOuterJoinLateral , fullOuter, fullOuterJoin, fullOuterJoinLateral ) where import Generics.SOP hiding (from) import qualified Generics.SOP as SOP import Squeal.PostgreSQL.Type.Alias import Squeal.PostgreSQL.Expression import Squeal.PostgreSQL.Expression.Logic import Squeal.PostgreSQL.Query import Squeal.PostgreSQL.Query.From import Squeal.PostgreSQL.Query.From.Set import Squeal.PostgreSQL.Render import Squeal.PostgreSQL.Type.List import Squeal.PostgreSQL.Type.Schema -- $setup -- >>> import Squeal.PostgreSQL {- | A `JoinItem` is the right hand side of a `cross`, `inner`, `leftOuter`, `rightOuter`, `fullOuter` join of `FromClause`s. -} data JoinItem (lat :: FromType) (with :: FromType) (db :: SchemasType) (params :: [NullType]) (left :: FromType) (right :: FromType) where Join :: FromClause lat with db params right -- ^ A standard `Squeal.PostgreSQL.Query.Join`. -- It is not allowed to reference columns provided -- by preceding `FromClause` items. -> JoinItem lat with db params left right JoinLateral :: Aliased (Query (Join lat left) with db params) query -- ^ Subqueries can be preceded by `JoinLateral`. -- This allows them to reference columns provided -- by preceding `FromClause` items. -> JoinItem lat with db params left '[query] JoinFunction :: SetFun db arg set -- ^ Set returning functions can be preceded by `JoinFunction`. -- This allows them to reference columns provided -- by preceding `FromClause` items. -> Expression 'Ungrouped lat with db params left arg -- ^ argument -> JoinItem lat with db params left '[set] JoinFunctionN :: SListI args => SetFunN db args set -- ^ Set returning multi-argument functions -- can be preceded by `JoinFunctionN`. -- This allows them to reference columns provided -- by preceding `FromClause` items. -> NP (Expression 'Ungrouped lat with db params left) args -- ^ arguments -> JoinItem lat with db params left '[set] instance RenderSQL (JoinItem lat with db params left right) where renderSQL = \case Join tab -> "JOIN" <+> renderSQL tab JoinLateral qry -> "JOIN LATERAL" <+> renderAliased (parenthesized . renderSQL) qry JoinFunction fun x -> "JOIN" <+> renderSQL (fun (UnsafeExpression (renderSQL x))) JoinFunctionN fun xs -> "JOIN" <+> renderSQL (fun (SOP.hmap (UnsafeExpression . renderSQL) xs)) {- | @left & cross (Join right)@. For every possible combination of rows from @left@ and @right@ (i.e., a Cartesian product), the joined table will contain a row consisting of all columns in @left@ followed by all columns in @right@. If the tables have @n@ and @m@ rows respectively, the joined table will have @n * m@ rows. -} cross :: JoinItem lat with db params left right -- ^ right -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (Join left right) cross item tab = UnsafeFromClause $ renderSQL tab <+> "CROSS" <+> renderSQL item {- | @left & crossJoin right@. For every possible combination of rows from @left@ and @right@ (i.e., a Cartesian product), the joined table will contain a row consisting of all columns in @left@ followed by all columns in @right@. If the tables have @n@ and @m@ rows respectively, the joined table will have @n * m@ rows. -} crossJoin :: FromClause lat with db params right -- ^ right -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (Join left right) crossJoin = cross . Join {- | Like `crossJoin` with a `subquery` but allowed to reference columns provided by preceding `FromClause` items. -} crossJoinLateral :: Aliased (Query (Join lat left) with db params) query -- ^ right subquery -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (Join left '[query]) crossJoinLateral = cross . JoinLateral {- | @left & inner (Join right) on@. The joined table is filtered by the @on@ condition. -} inner :: JoinItem lat with db params left right -- ^ right -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (Join left right) inner item on tab = UnsafeFromClause $ renderSQL tab <+> "INNER" <+> renderSQL item <+> "ON" <+> renderSQL on {- | @left & innerJoin right on@. The joined table is filtered by the @on@ condition. -} innerJoin :: FromClause lat with db params right -- ^ right -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (Join left right) innerJoin = inner . Join {- | Like `innerJoin` with a `subquery` but allowed to reference columns provided by preceding `FromClause` items. -} innerJoinLateral :: Aliased (Query (Join lat left) with db params) query -- ^ right subquery -> Condition 'Ungrouped lat with db params (Join left '[query]) -- ^ @ON@ condition -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (Join left '[query]) innerJoinLateral = inner . JoinLateral {- | @left & leftOuter (Join right) on@. First, an inner join is performed. Then, for each row in @left@ that does not satisfy the @on@ condition with any row in @right@, a joined row is added with null values in columns of @right@. Thus, the joined table always has at least one row for each row in @left@. -} leftOuter :: JoinItem lat with db params left right -- ^ right -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (Join left (NullifyFrom right)) leftOuter item on tab = UnsafeFromClause $ renderSQL tab <+> "LEFT OUTER" <+> renderSQL item <+> "ON" <+> renderSQL on {- | @left & leftOuterJoin right on@. First, an inner join is performed. Then, for each row in @left@ that does not satisfy the @on@ condition with any row in @right@, a joined row is added with null values in columns of @right@. Thus, the joined table always has at least one row for each row in @left@. -} leftOuterJoin :: FromClause lat with db params right -- ^ right -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (Join left (NullifyFrom right)) leftOuterJoin = leftOuter . Join {- | Like `leftOuterJoin` with a `subquery` but allowed to reference columns provided by preceding `FromClause` items. -} leftOuterJoinLateral :: Aliased (Query (Join lat left) with db params) query -- ^ right subquery -> Condition 'Ungrouped lat with db params (Join left '[query]) -- ^ @ON@ condition -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (Join left (NullifyFrom '[query])) leftOuterJoinLateral = leftOuter . JoinLateral {- | @left & rightOuter (Join right) on@. First, an inner join is performed. Then, for each row in @right@ that does not satisfy the @on@ condition with any row in @left@, a joined row is added with null values in columns of @left@. This is the converse of a left join: the result table will always have a row for each row in @right@. -} rightOuter :: JoinItem lat with db params left right -- ^ right -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (Join (NullifyFrom left) right) rightOuter item on tab = UnsafeFromClause $ renderSQL tab <+> "RIGHT OUTER" <+> renderSQL item <+> "ON" <+> renderSQL on {- | @left & rightOuterJoin right on@. First, an inner join is performed. Then, for each row in @right@ that does not satisfy the @on@ condition with any row in @left@, a joined row is added with null values in columns of @left@. This is the converse of a left join: the result table will always have a row for each row in @right@. -} rightOuterJoin :: FromClause lat with db params right -- ^ right -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (Join (NullifyFrom left) right) rightOuterJoin = rightOuter . Join {- | Like `rightOuterJoin` with a `subquery` but allowed to reference columns provided by preceding `FromClause` items. -} rightOuterJoinLateral :: Aliased (Query (Join lat left) with db params) query -- ^ right subquery -> Condition 'Ungrouped lat with db params (Join left '[query]) -- ^ @ON@ condition -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (Join (NullifyFrom left) '[query]) rightOuterJoinLateral = rightOuter . JoinLateral {- | @left & fullOuter (Join right) on@. First, an inner join is performed. Then, for each row in @left@ that does not satisfy the @on@ condition with any row in @right@, a joined row is added with null values in columns of @right@. Also, for each row of @right@ that does not satisfy the join condition with any row in @left@, a joined row with null values in the columns of @left@ is added. -} fullOuter :: JoinItem lat with db params left right -- ^ right -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (NullifyFrom (Join left right)) fullOuter item on tab = UnsafeFromClause $ renderSQL tab <+> "FULL OUTER" <+> renderSQL item <+> "ON" <+> renderSQL on {- | @left & fullOuterJoin right on@. First, an inner join is performed. Then, for each row in @left@ that does not satisfy the @on@ condition with any row in @right@, a joined row is added with null values in columns of @right@. Also, for each row of @right@ that does not satisfy the join condition with any row in @left@, a joined row with null values in the columns of @left@ is added. -} fullOuterJoin :: FromClause lat with db params right -- ^ right -> Condition 'Ungrouped lat with db params (Join left right) -- ^ @ON@ condition -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (NullifyFrom (Join left right)) fullOuterJoin = fullOuter . Join {- | Like `fullOuterJoin` with a `subquery` but allowed to reference columns provided by preceding `FromClause` items. -} fullOuterJoinLateral :: Aliased (Query (Join lat left) with db params) query -- ^ right subquery -> Condition 'Ungrouped lat with db params (Join left '[query]) -- ^ @ON@ condition -> FromClause lat with db params left -- ^ left -> FromClause lat with db params (NullifyFrom (Join left '[query])) fullOuterJoinLateral = fullOuter . JoinLateral