diff --git a/esqueleto.cabal b/esqueleto.cabal index 921472e..57dad3b 100644 --- a/esqueleto.cabal +++ b/esqueleto.cabal @@ -38,6 +38,9 @@ description: functions), so please open an issue or send a pull request if you need anything that is not covered by @esqueleto@ on . + . + The name of this library means \"skeleton\" in Portuguese and + contains all three SQL letters in the correct order =). source-repository head type: git diff --git a/src/Database/Esqueleto.hs b/src/Database/Esqueleto.hs index 8e55ca7..1cb488e 100644 --- a/src/Database/Esqueleto.hs +++ b/src/Database/Esqueleto.hs @@ -1,21 +1,23 @@ {-# LANGUAGE FlexibleContexts, FlexibleInstances, GADTs #-} --- | Main module of @esqueleto@. This module replaces --- @Database.Persist@, so instead of importing that module you --- should just import this one: +-- | The @esqueleto@ EDSL (embedded domain specific language). +-- This module replaces @Database.Persist@, so instead of +-- importing that module you should just import this one: -- -- @ -- import Database.Esqueleto --- @ --- --- If you still have legacy code using @Database.Persist.Query@ --- (which is exported by @Database.Persist@), you may import it --- qualified: --- --- @ -- import qualified Database.Persist.Query as OldQuery -- @ module Database.Esqueleto - ( -- * @esqueleto@'s Language + ( -- * Setup + -- $setup + + -- * Introduction + -- $introduction + + -- * Getting started + -- $gettingstarted + + -- * @esqueleto@'s Language Esqueleto( where_, on, orderBy, asc, desc , sub_select, sub_selectDistinct, (^.), (?.) , val, isNothing, just, nothing, countRows, not_ @@ -55,6 +57,232 @@ import Database.Persist.Store hiding (delete) import Database.Persist.GenericSql import qualified Database.Persist.Store +-- $setup +-- +-- If you're already using @persistent@, then you're ready to use +-- @esqueleto@, no further setup is needed. If you're just +-- starting a new project and would like to use @esqueleto@, take +-- a look at @persistent@'s book first +-- () to learn how to +-- define your schema. + +---------------------------------------------------------------------- + +-- $introduction +-- +-- The main goals of @esqueleto@ are to: +-- +-- * Be easily translatable to SQL. When you take a look at a +-- @esqueleto@ query, you should be able to know exactly how +-- the SQL query will end up. (As opposed to being a +-- relational algebra EDSL such as HaskellDB, which is +-- non-trivial to translate into SQL.) +-- +-- * Support the mostly used SQL features. We'd like you to be +-- able to use @esqueleto@ for all of your queries, no +-- exceptions. Send a pull request or open an issue on our +-- project page () if +-- there's anything missing that you'd like to see. +-- +-- * Be as type-safe as possible. There are ways of shooting +-- yourself in the foot while using @esqueleto@ because it's +-- extremely hard to provide 100% type-safety into a SQL-like +-- EDSL---there's a tension between supporting features with a +-- nice syntax and rejecting bad code. However, we strive to +-- provide as many type checks as possible. If you get bitten +-- by some invalid code that type-checks, please open an issue +-- on our project page so we can take a look. + +---------------------------------------------------------------------- + +-- $gettingstarted +-- +-- We like clean, easy-to-read EDSLs. However, in order to +-- achieve this goal we've used a lot of type hackery, leading to +-- some hard-to-read type signatures. On this section, we'll try +-- to build some intuition about the syntax. +-- +-- For the following examples, we'll use this example schema: +-- +-- @ +-- 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 +-- |] +-- @ +-- +-- Most of @esqueleto@ was created with @SELECT@ statements in +-- mind, not only because they're the most common but also +-- because they're the most complex kind of statement. The most +-- simple kind of @SELECT@ would be: +-- +-- @ +-- SELECT * +-- FROM Person +-- @ +-- +-- In @esqueleto@, we may write the same query above as: +-- +-- @ +-- do people <- 'select' $ +-- 'from' $ \\person -> do +-- return person +-- liftIO $ mapM_ (putStrLn . personName . entityVal) people +-- @ +-- +-- The expression above has type @SqlPersist m ()@, while +-- @people@ has type @[Entity Person]@. The query above will be +-- translated into exactly the same query we wrote manually, but +-- instead of @SELECT *@ it will list all entity fields (using +-- @*@ is not robust). Note that @esqueleto@ knows that we want +-- an @Entity Person@ just because of the @personName@ that we're +-- printing later. +-- +-- However, most of the time we need to filter our queries using +-- @WHERE@. For example: +-- +-- @ +-- SELECT * +-- FROM Person +-- WHERE Person.name = \"John\" +-- @ +-- +-- In @esqueleto@, we may write the same query above as: +-- +-- @ +-- select $ +-- from $ \\p -> do +-- 'where_' (p '^.' PersonName '==.' 'val' \"John\") +-- return p +-- @ +-- +-- Although @esqueleto@'s code is a bit more noisy, it's has +-- almost the same structure (save from the @return@). The +-- @('^.')@ operator is used to project a field from an entity. +-- The field name is the same one generated by @persistent@'s +-- Template Haskell functions. We use 'val' to lift a constant +-- Haskell value into the SQL query. +-- +-- Another example would be: +-- +-- @ +-- SELECT * +-- FROM Person +-- WHERE Person.age >= 18 +-- @ +-- +-- In @esqueleto@, we may write the same query above as: +-- +-- @ +-- select $ +-- from $ \\p -> do +-- where_ (p ^. PersonAge '>=.' 'just' (val 18)) +-- return p +-- @ +-- +-- Since @age@ is an optional @Person@ field, we use 'just' lift +-- @val 18 :: SqlExpr (Single Int)@ into @just (val 18) :: +-- SqlExpr (Single (Just Int))@. +-- +-- Implicit joins are represented by tuples. For example, to get +-- the list of all blog posts and their authors, we could write: +-- +-- @ +-- SELECT BlogPost.*, Person.* +-- FROM BlogPost, Person +-- WHERE BlogPost.authorId = Person.id +-- ORDER BY BlogPost.title ASC +-- @ +-- +-- In @esqueleto@, we may write the same query above as: +-- +-- @ +-- select $ +-- from $ \\(b, p) -> do +-- where_ (b ^. BlogPostAuthorId ==. p ^. PersonId) +-- 'orderBy' ['asc' (b ^. BlogPostTitle)] +-- return (b, p) +-- @ +-- +-- However, we may want your results to include people who don't +-- have any blog posts as well using a @LEFT OUTER JOIN@: +-- +-- @ +-- SELECT Person.*, BlogPost.* +-- FROM Person LEFT OUTER JOIN BlogPost +-- ON Person.id = BlogPost.authorId +-- ORDER BY Person.name ASC, BlogPost.title ASC +-- @ +-- +-- In @esqueleto@, we may write the same query above as: +-- +-- @ +-- select $ +-- from $ \\(p ``LeftOuterJoin`` mb) -> do +-- 'on' (just (p ^. PersonId) ==. mb '?.' BlogPostAuthorId) +-- orderBy [asc (p ^. PersonName), asc (mb '?.' BlogPostTitle)] +-- return (p, mb) +-- @ +-- +-- On a @LEFT OUTER JOIN@ the entity on the right hand side may +-- not exist (i.e. there may be a @Person@ without any +-- @BlogPost@s), so while @p :: SqlExpr (Entity Person)@, we have +-- @mb :: SqlExpr (Maybe (Entity BlogPost))@. The whole +-- expression above has type @SqlPersist m [(Entity Person, Maybe +-- (Entity BlogPost))]@. Instead of using @(^.)@, we used +-- @('?.')@ to project a field from a @Maybe (Entity a)@. +-- +-- We are by no means limited to joins of two tables, nor by +-- joins of different tables. For example, we may want a list +-- the @Follow@ entity: +-- +-- @ +-- SELECT P1.*, Follow.*, P2.* +-- FROM Person AS P1 +-- INNER JOIN Follow ON P1.id = Follow.follower +-- INNER JOIN P2 ON P2.id = Follow.followed +-- @ +-- +-- In @esqueleto@, we may write the same query above as: +-- +-- @ +-- select $ +-- from $ \\(p1 ``InnerJoin`` f ``InnerJoin`` p2) -> do +-- on (p2 ^. PersonId ==. f ^. FollowFollowed) +-- on (p1 ^. PersonId ==. f ^. FollowFollower) +-- return (p1, f, p2) +-- @ +-- +-- /Note carefully that the order of the ON clauses is/ +-- /reversed!/ You're required to write your 'on's in reverse +-- order because that helps composability (see the documention of +-- 'on' for more details). +-- +-- We also currently supports @UPDATE@ and @DELETE@ statements. +-- For example: +-- +-- @ +-- do 'update' $ \\p -> do +-- 'set' p [ PersonName '=.' val \"João\" ] +-- where_ (p ^. PersonName ==. val \"Joao\") +-- 'delete' $ +-- from $ \\p -> do +-- where_ (p ^. PersonAge <. just (val 14)) +-- @ + + +---------------------------------------------------------------------- + -- | Synonym for 'Database.Persist.Store.delete' that does not -- clash with @esqueleto@'s 'delete'.