esqueleto/src/Database/Esqueleto/Experimental.hs
Matt Parsons b295bc6a5f
Esqueleto.Legacy (#259)
* Esqueleto.Legacy

* Add changelog entry

* Delete deprecated modules

* a bit more

* ghc 9 support, clean warns

* yes

* okkk
2021-05-26 14:27:04 -06:00

563 lines
13 KiB
Haskell

{-# LANGUAGE PatternSynonyms #-}
-- | This module contains a new way (introduced in 3.3.3.0) of using @FROM@ in
-- Haskell. The old method was a bit finicky and could permit runtime errors,
-- and this new way is both significantly safer and much more powerful.
--
-- This syntax will become the default syntax exported from the library in
-- version @3.6.0.0@. To use the old syntax, see "Database.Esqueleto.Legacy".
module Database.Esqueleto.Experimental
( -- * Setup
-- $setup
-- * Introduction
-- $introduction
-- * A New Syntax
-- $new-syntax
-- * Documentation
-- ** Basic Queries
from
, table
, Table(..)
, SubQuery(..)
, selectQuery
-- ** Joins
, (:&)(..)
, on
, innerJoin
, innerJoinLateral
, leftJoin
, leftJoinLateral
, rightJoin
, fullOuterJoin
, crossJoin
, crossJoinLateral
-- ** Set Operations
-- $sql-set-operations
, union_
, Union(..)
, unionAll_
, UnionAll(..)
, except_
, Except(..)
, intersect_
, Intersect(..)
, pattern SelectQuery
-- ** Common Table Expressions
, with
, withRecursive
-- ** Internals
, From(..)
, ToMaybe(..)
, ToAlias(..)
, ToAliasT
, ToAliasReference(..)
, ToAliasReferenceT
, ToSqlSetOperation(..)
-- * The Normal Stuff
, where_
, groupBy
, orderBy
, rand
, asc
, desc
, limit
, offset
, distinct
, distinctOn
, don
, distinctOnOrderBy
, having
, locking
, sub_select
, (^.)
, (?.)
, val
, isNothing
, just
, nothing
, joinV
, withNonNull
, countRows
, count
, countDistinct
, not_
, (==.)
, (>=.)
, (>.)
, (<=.)
, (<.)
, (!=.)
, (&&.)
, (||.)
, between
, (+.)
, (-.)
, (/.)
, (*.)
, random_
, round_
, ceiling_
, floor_
, min_
, max_
, sum_
, avg_
, castNum
, castNumM
, coalesce
, coalesceDefault
, lower_
, upper_
, trim_
, ltrim_
, rtrim_
, length_
, left_
, right_
, like
, ilike
, (%)
, concat_
, (++.)
, castString
, subList_select
, valList
, justList
, in_
, notIn
, exists
, notExists
, set
, (=.)
, (+=.)
, (-=.)
, (*=.)
, (/=.)
, case_
, toBaseId
, subSelect
, subSelectMaybe
, subSelectCount
, subSelectForeign
, subSelectList
, subSelectUnsafe
, ToBaseId(..)
, when_
, then_
, else_
, Value(..)
, ValueList(..)
, OrderBy
, DistinctOn
, LockingKind(..)
, SqlString
-- ** Joins
, InnerJoin(..)
, CrossJoin(..)
, LeftOuterJoin(..)
, RightOuterJoin(..)
, FullOuterJoin(..)
, JoinKind(..)
, OnClauseWithoutMatchingJoinException(..)
-- ** SQL backend
, SqlQuery
, SqlExpr
, SqlEntity
, select
, selectSource
, delete
, deleteCount
, update
, updateCount
, insertSelect
, insertSelectCount
, (<#)
, (<&>)
-- ** Rendering Queries
, renderQueryToText
, renderQuerySelect
, renderQueryUpdate
, renderQueryDelete
, renderQueryInsertInto
-- ** Helpers
, valkey
, valJ
, associateJoin
-- ** Re-exports
-- $reexports
, deleteKey
, module Database.Esqueleto.Internal.PersistentImport
) where
import Database.Esqueleto.Internal.Internal hiding (From, from, on)
import Database.Esqueleto.Internal.PersistentImport
import Database.Esqueleto.Experimental.From
import Database.Esqueleto.Experimental.From.CommonTableExpression
import Database.Esqueleto.Experimental.From.Join
import Database.Esqueleto.Experimental.From.SqlSetOperation
import Database.Esqueleto.Experimental.ToAlias
import Database.Esqueleto.Experimental.ToAliasReference
import Database.Esqueleto.Experimental.ToMaybe
-- $setup
--
-- If you're already using "Database.Esqueleto", then you can get
-- started using this module just by changing your imports slightly,
-- as well as enabling the [TypeApplications](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#extension-TypeApplications) extension.
--
-- @
-- {-\# LANGUAGE TypeApplications \#-}
--
-- ...
--
-- import Database.Esqueleto.Experimental
-- @
--
-- Note: Prior to @esqueleto-3.3.4.0@, the @Database.Esqueleto.Experimental@
-- module did not reexport @Data.Esqueleto@.
----------------------------------------------------------------------
-- $introduction
--
-- This module is fully backwards-compatible extension to the @esqueleto@
-- EDSL that expands subquery functionality and enables
-- [SQL set operations](https://en.wikipedia.org/wiki/Set_operations_(SQL\))
-- to be written directly in Haskell. Specifically, this enables:
--
-- * Subqueries in 'JOIN' statements
-- * 'UNION'
-- * 'UNION' 'ALL'
-- * 'INTERSECT'
-- * 'EXCEPT'
--
-- As a consequence of this, several classes of runtime errors are now
-- caught at compile time. This includes missing 'on' clauses and improper
-- handling of @Maybe@ values in outer joins.
--
-- This module can be used in conjunction with the main "Database.Esqueleto"
-- module, but doing so requires qualified imports to avoid ambiguous
-- definitions of 'on' and 'from', which are defined in both modules.
--
-- Below we will give an overview of how to use this module and the
-- features it enables.
----------------------------------------------------------------------
-- $new-syntax
--
-- This module introduces a new syntax that serves to enable the aforementioned
-- features. This new syntax also changes how joins written in the @esqueleto@
-- EDSL to more closely resemble the underlying SQL.
--
-- For our examples, we'll use a schema similar to the one in the Getting Started
-- section of "Database.Esqueleto":
--
-- @
-- share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
-- Person
-- name String
-- age Int Maybe
-- deriving Eq Show
-- BlogPost
-- title String
-- authorId PersonId
-- deriving Eq Show
-- Follow
-- follower PersonId
-- followed PersonId
-- deriving Eq Show
-- |]
-- @
--
-- === Example 1: Simple select
--
-- Let's select all people who are named \"John\".
--
-- ==== "Database.Esqueleto":
--
-- @
-- select $
-- from $ \\people -> do
-- where_ (people ^. PersonName ==. val \"John\")
-- pure people
-- @
--
-- ==== "Database.Esqueleto.Experimental":
--
-- @
-- select $ do
-- people <- from $ table \@Person
-- where_ (people ^. PersonName ==. val \"John\")
-- pure people
-- @
--
--
-- === Example 2: Select with join
--
-- Let's select all people and their blog posts who are over
-- the age of 18.
--
-- ==== "Database.Esqueleto":
--
-- @
-- select $
-- from $ \\(people \`LeftOuterJoin\` blogPosts) -> do
-- on (people ^. PersonId ==. blogPosts ?. BlogPostAuthorId)
-- where_ (people ^. PersonAge >. val 18)
-- pure (people, blogPosts)
-- @
--
-- ==== "Database.Esqueleto.Experimental":
--
-- Here we use the ':&' operator to pattern match against the joined tables.
--
-- @
-- select $ do
-- (people :& blogPosts) <-
-- from $ table \@Person
-- \`leftJoin\` table \@BlogPost
-- \`on\` (\\(people :& blogPosts) ->
-- people ^. PersonId ==. blogPosts ?. BlogPostAuthorId)
-- where_ (people ^. PersonAge >. val 18)
-- pure (people, blogPosts)
-- @
--
-- === Example 3: Select with multi-table join
--
-- Let's select all people who follow a person named \"John\", including
-- the name of each follower.
--
-- ==== "Database.Esqueleto":
--
-- @
-- select $
-- from $ \\(
-- people1
-- \`InnerJoin\` followers
-- \`InnerJoin\` people2
-- ) -> do
-- on (people1 ^. PersonId ==. followers ^. FollowFollowed)
-- on (followers ^. FollowFollower ==. people2 ^. PersonId)
-- where_ (people1 ^. PersonName ==. val \"John\")
-- pure (followers, people2)
-- @
--
-- ==== "Database.Esqueleto.Experimental":
--
-- In this version, with each successive 'on' clause, only the tables
-- we have already joined into are in scope, so we must pattern match
-- accordingly. In this case, in the second 'innerJoin', we do not use
-- the first `Person` reference, so we use @_@ as a placeholder to
-- ignore it. This prevents a possible runtime error where a table
-- is referenced before it appears in the sequence of 'JOIN's.
--
-- @
-- select $ do
-- (people1 :& followers :& people2) <-
-- from $ table \@Person
-- \`innerJoin` table \@Follow
-- \`on\` (\\(people1 :& followers) ->
-- people1 ^. PersonId ==. followers ^. FollowFollowed)
-- \`innerJoin` table \@Person
-- \`on\` (\\(_ :& followers :& people2) ->
-- followers ^. FollowFollower ==. people2 ^. PersonId)
-- where_ (people1 ^. PersonName ==. val \"John\")
-- pure (followers, people2)
-- @
--
-- === Example 4: Counting results of a subquery
--
-- Let's count the number of people who have posted at least 10 posts
--
-- ==== "Database.Esqueleto":
--
-- @
-- select $ pure $ subSelectCount $
-- from $ \\(
-- people
-- \`InnerJoin\` blogPosts
-- ) -> do
-- on (people ^. PersonId ==. blogPosts ^. BlogPostAuthorId)
-- groupBy (people ^. PersonId)
-- having ((count $ blogPosts ^. BlogPostId) >. val 10)
-- pure people
-- @
--
-- ==== "Database.Esqueleto.Experimental":
--
-- @
-- select $ do
-- peopleWithPosts <-
-- from $ do
-- (people :& blogPosts) <-
-- from $ table \@Person
-- \`innerJoin\` table \@BlogPost
-- \`on\` (\\(p :& bP) ->
-- p ^. PersonId ==. bP ^. BlogPostAuthorId)
-- groupBy (people ^. PersonId)
-- having ((count $ blogPosts ^. BlogPostId) >. val 10)
-- pure people
-- pure $ count (peopleWithPosts ^. PersonId)
-- @
--
-- We now have the ability to refactor this
--
-- === Example 5: Sorting the results of a UNION with limits
--
-- Out of all of the posts created by a person and the people they follow,
-- generate a list of the first 25 posts, sorted alphabetically.
--
-- ==== "Database.Esqueleto":
--
-- Since 'UNION' is not supported, this requires using `Database.Esqueleto.rawSql`. (Not shown)
--
-- ==== "Database.Esqueleto.Experimental":
--
-- Since this module supports all set operations (see `SqlSetOperation`), we can use
-- `Union` to write this query.
--
-- @
-- select $ do
-- (authors, blogPosts) <- from $
-- (do
-- (author :& blogPost) <-
-- from $ table \@Person
-- \`innerJoin\` table \@BlogPost
-- \`on\` (\\(a :& bP) ->
-- a ^. PersonId ==. bP ^. BlogPostAuthorId)
-- where_ (author ^. PersonId ==. val currentPersonId)
-- pure (author, blogPost)
-- )
-- \`union_\`
-- (do
-- (follow :& blogPost :& author) <-
-- from $ table \@Follow
-- \`innerJoin\` table \@BlogPost
-- \`on\` (\\(f :& bP) ->
-- f ^. FollowFollowed ==. bP ^. BlogPostAuthorId)
-- \`innerJoin\` table \@Person
-- \`on\` (\\(_ :& bP :& a) ->
-- bP ^. BlogPostAuthorId ==. a ^. PersonId)
-- where_ (follow ^. FollowFollower ==. val currentPersonId)
-- pure (author, blogPost)
-- )
-- orderBy [ asc (blogPosts ^. BlogPostTitle) ]
-- limit 25
-- pure (authors, blogPosts)
-- @
--
-- === Example 6: LATERAL JOIN
--
-- As of version @3.4.0.0@, lateral subquery joins are supported.
--
--
-- @
-- select $ do
-- (salesPerson :& maxSaleAmount :& maxSaleCustomerName) <-
-- from $ table \@SalesPerson
-- \`crossJoinLateral\` (\\salesPerson -> do
-- sales <- from $ table \@Sale
-- where_ $ sales ^. SaleSalesPersonId ==. salesPerson ^. SalesPersonId
-- pure $ max_ (sales ^. SaleAmount)
-- )
-- \`crossJoinLateral\` (\\(salesPerson :& maxSaleAmount) -> do
-- sales <- from $ table \@Sale
-- where_ $ sales ^. SaleSalesPersonId ==. salesPerson ^. SalesPersonId
-- &&. sales ^. SaleAmount ==. maxSaleAmount
-- pure $ sales ^. SaleCustomerName)
-- )
-- pure (salesPerson ^. SalesPersonName, maxSaleAmount, maxSaleCustomerName)
-- @
--
-- This is the equivalent to the following SQL (example taken from the
-- [MySQL Lateral Derived Table](https://dev.mysql.com/doc/refman/8.0/en/lateral-derived-tables.html)
-- documentation):
--
-- @
-- SELECT
-- salesperson.name,
-- max_sale.amount,
-- max_sale_customer.customer_name
-- FROM
-- salesperson,
-- -- calculate maximum size, cache it in transient derived table max_sale
-- LATERAL
-- (SELECT MAX(amount) AS amount
-- FROM all_sales
-- WHERE all_sales.salesperson_id = salesperson.id)
-- AS max_sale,
-- LATERAL
-- (SELECT customer_name
-- FROM all_sales
-- WHERE all_sales.salesperson_id = salesperson.id
-- AND all_sales.amount =
-- -- the cached maximum size
-- max_sale.amount)
-- AS max_sale_customer;
-- @
-- $sql-set-operations
--
-- Data type that represents SQL set operations. This includes
-- 'UNION', 'UNION' 'ALL', 'EXCEPT', and 'INTERSECT'. These types form
-- a binary tree, with @SqlQuery@ values on the leaves.
--
-- Each function corresponding to the aforementioned set operations
-- can be used as an infix in a @from@ to help with readability
-- and lead to code that closely resembles the underlying SQL. For example,
--
-- @
-- select $ from $
-- (do
-- a <- from $ table @A
-- pure $ a ^. ASomeCol
-- )
-- \`union_\`
-- (do
-- b <- from $ table @B
-- pure $ b ^. BSomeCol
-- )
-- @
--
-- is translated into
--
-- @
-- SELECT * FROM (
-- (SELECT a.some_col FROM a)
-- UNION
-- (SELECT b.some_col FROM b)
-- )
-- @
--