* Esqueleto.Legacy * Add changelog entry * Delete deprecated modules * a bit more * ghc 9 support, clean warns * yes * okkk
563 lines
13 KiB
Haskell
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)
|
|
-- )
|
|
-- @
|
|
--
|