Type-safe EDSL for SQL queries on persistent backends.

Version on this page:
LTS Haskell 22.28:
Stackage Nightly 2024-07-13:
Latest on Hackage:

See all snapshots esqueleto appears in

BSD-3-Clause licensed by Felipe Lessa
Maintained by [email protected]
This version can be pinned in stack with:esqueleto-,5402

Esqueleto TravisCI

Skeleton Image courtesy Chrissy Long

Esqueleto, a SQL DSL for Haskell

Esqueleto is a bare bones, type-safe EDSL for SQL queries that works with unmodified persistent SQL backends. The name of this library means “skeleton” in Portuguese and contains all three SQL letters in the correct order =). It was inspired by Scala’s Squeryl but created from scratch. Its language closely resembles SQL. Currently, SELECTs, UPDATEs, INSERTs and DELETEs are supported.

In particular, esqueleto is the recommended library for type-safe JOINs on persistent SQL backends. (The alternative is using raw SQL, but that’s error prone and does not offer any composability.). For more information read esqueleto.


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.

If you need to use persistent’s default support for queries as well, either import it qualified:

-- For a module that mostly uses esqueleto.
import Database.Esqueleto
import qualified Database.Persistent as P

or import esqueleto itself qualified:

-- For a module that uses esqueleto just on some queries.
import Database.Persistent
import qualified Database.Esqueleto as E

Other than identifier name clashes, esqueleto does not conflict with persistent in any way.


The main goals of esqueleto are:

  • Be easily translatable to SQL. (You should be able to know exactly how the SQL query will end up.)
  • Support the most widely used SQL features.
  • Be as type-safe as possible.

It is not a goal to be able to write portable SQL. We do not try to hide the differences between DBMSs from you


For the following examples, we’ll use this example schema:

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
    name String
    age Int Maybe
    deriving Eq Show
    title String
    authorId PersonId
    deriving Eq Show
    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:

putPersons :: SqlPersist m ()
putPersons = do
  people <- select $
              from $ \person -> do
              return person
  liftIO $ mapM_ (putStrLn . personName . entityVal) people

which generates this SQL:

FROM Person

esqueleto knows that we want an Entity Person just because of the personName that is printed.


Filtering by PersonName:

select $
from $ \p -> do
where_ (p ^. PersonName ==. val "John")
return p

which generates this SQL:

FROM Person
WHERE = "John"

The (^.) operator is used to project a field from an entity. The field name is the same one generated by persistents Template Haskell functions. We use val to lift a constant Haskell value into the SQL query.

Another example:

In esqueleto, we may write the same query above as:

select $
from $ \p -> do
where_ (p ^. PersonAge >=. just (val 18))
return p

which generates this SQL:

FROM Person
WHERE Person.age >= 18

Since age is an optional Person field, we use just to liftval 18 :: SqlExpr (Value Int) into just (val 18) ::SqlExpr (Value (Maybe Int)).


Implicit joins are represented by tuples.

For example, to get the list of all blog posts and their authors, we could write:

select $
from $ \(b, p) -> do
where_ (b ^. BlogPostAuthorId ==. p ^. PersonId)
orderBy [asc (b ^. BlogPostTitle)]
return (b, p)

which generates this SQL:

SELECT BlogPost.*, Person.*
FROM BlogPost, Person
WHERE BlogPost.authorId =
ORDER BY BlogPost.title ASC

However, you may want your results to include people who don’t have any blog posts as well using a LEFT OUTER JOIN:

select $
from $ \(p `LeftOuterJoin` mb) -> do
on (just (p ^. PersonId) ==. mb ?. BlogPostAuthorId)
orderBy [asc (p ^. PersonName), asc (mb ?. BlogPostTitle)]
return (p, mb)

which generates this SQL:

SELECT Person.*, BlogPost.*
ON = BlogPost.authorId
ORDER BY ASC, BlogPost.title ASC

Left Outer Join

On a LEFT OUTER JOIN the entity on the right hand side may not exist (i.e. there may be a Person without any BlogPosts), 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 of the Follow entity:

select $
from $ \(p1 `InnerJoin` f `InnerJoin` p2) -> do
on (p2 ^. PersonId ==. f ^. FollowFollowed)
on (p1 ^. PersonId ==. f ^. FollowFollower)
return (p1, f, p2)

which generates this SQL:

SELECT P1.*, Follow.*, P2.*
FROM Person AS P1
INNER JOIN Follow ON = Follow.follower
INNER JOIN Person AS P2 ON = Follow.followed

Note carefully that the order of the ON clauses is reversed! You’re required to write your ons in reverse order because that helps composability (see the documentation of on for more details).

Update and Delete

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))

The results of queries can also be used for insertions. In SQL, we might write the following, inserting a new blog post for every user:

 insertSelect $ from $ \p->
 return $ BlogPost <# "Group Blog Post" <&> (p ^. PersonId)

which generates this SQL:

SELECT ('Group Blog Post', id)
FROM Person

Individual insertions can be performed through Persistent’s insert function, reexported for convenience.


We re-export many symbols from persistent for convenience:

  • “Store functions” from “Database.Persist”.
  • Everything from “Database.Persist.Class” except for PersistQuery and delete (use deleteKey instead).
  • Everything from “Database.Persist.Types” except for Update, SelectOpt, BackendSpecificFilter and Filter.
  • Everything from “Database.Persist.Sql” except for deleteWhereCount and updateWhereCount.

RDBMS Specific

There are many differences between SQL syntax and functions supported by different RDBMSs. Since version 2.2.8, esqueleto includes modules containing functions that are specific to a given RDBMS.

  • PostgreSQL: Database.Esqueleto.PostgreSQL
  • MySQL: Database.Esqueleto.MySQL
  • SQLite: Database.Esqueleto.SQLite

In order to use these functions, you need to explicitly import their corresponding modules.

Unsafe functions, operators and values

Esqueleto doesn’t support every possible function, and it can’t - many functions aren’t available on every RDBMS platform, and sometimes the same functionality is hidden behind different names. To overcome this problem, Esqueleto exports a number of unsafe functions to call any function, operator or value. These functions can be found in Database.Esqueleto.Internal.Sql module.

Warning: the functions discussed in this section must always be used with an explicit type signature,and the user must be careful to provide a type signature that corresponds correctly with the underlying code. The functions have extremely general types, and if you allow type inference to figure everything out for you, it may not correspond with the underlying SQL types that you want. This interface is effectively the FFI to SQL database, so take care!

The most common use of these functions is for calling RDBMS specific or custom functions, for that end we use unsafeSqlFunction. For example, if we wish to consult the postgres now function we could so as follow:

postgresTime :: (MonadIO m, MonadLogger m) => SqlWriteT m UTCTime
postgresTime = 
  result <- select (pure now)
  case result of
    [x] -> pure x
    _ -> error "now() is guaranteed to return a single result"
    now :: SqlExpr (Value UTCTime) 
    now = unsafeSqlFunction "now" ()

which generates this SQL:

SELECT now()

With the now function we could now use the current time of the postgres RDBMS on any query. Do notice that now does not use any arguments, so we use () that is an instance of UnsafeSqlFunctionArgument to represent no arguments, an empty list cast to a correct value will yield the same result as ().

We can also use unsafeSqlFunction for more complex functions with customs values using unsafeSqlValue which turns any string into a sql value of whatever type we want, disclaimer: if you use it badly you will cause a runtime error. For example, say we want to try postgres’ date_part function and get the day of a timestamp, we could use:

postgresTimestampDay :: (MonadIO m, MonadLogger m) => SqlWriteT m Int
postgresTimestampDay = 
  result <- select (return $ dayPart date)
  case result of
    [x] -> pure x
    _ -> error "dayPart is guaranteed to return a single result"
    dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int) 
    dayPart s = unsafeSqlFunction "date_part" (unsafeSqlValue "\'day\'" :: SqlExpr (Value String) ,s)
    date :: SqlExpr (Value UTCTime)
    date = unsafeSqlValue "TIMESTAMP \'2001-02-16 20:38:40\'"

which generates this SQL:

SELECT date_part('day', TIMESTAMP '2001-02-16 20:38:40')

Using unsafeSqlValue we were required to also define the type of the value.

Another useful unsafe function is unsafeSqlCastAs, which allows us to cast any type to another within a query. For example, say we want to use our previews dayPart function on the current system time, we could:

postgresTimestampDay :: (MonadIO m, MonadLogger m) => SqlWriteT m Int
postgresTimestampDay = do
  currentTime <- liftIO getCurrentTime
  result <- select (return $ dayPart (toTIMESTAMP $ val currentTime))
  case result of
    [x] -> pure x
    _ -> error "dayPart is guaranteed to return a single result"
    dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int) 
    dayPart s = unsafeSqlFunction "date_part" (unsafeSqlValue "\'day\'" :: SqlExpr (Value String) ,s)
    toTIMESTAMP :: SqlExpr (Value UTCTime) -> SqlExpr (Value UTCTime)
    toTIMESTAMP = unsafeSqlCastAs "TIMESTAMP"

which generates this SQL:

SELECT date_part('day', CAST('2019-10-28 23:19:39.400898344Z' AS TIMESTAMP))

SQL injection

Esqueleto uses parameterization to prevent sql injections on values and arguments on all queries, for example, if we have:

myEvilQuery :: (MonadIO m, MonadLogger m) => SqlWriteT m ()
myEvilQuery = 
  select (return $ val ("hi\'; DROP TABLE foo; select \'bye\'" :: String)) >>= liftIO . print

which generates this SQL(when using postgres):

SELECT 'hi''; DROP TABLE foo; select ''bye'''

And the printed value is hi\'; DROP TABLE foo; select \'bye\' and no table is dropped. This is good and makes the use of strings values safe. Unfortunately this is not the case when using unsafe functions. Let’s see an example of defining a new evil now function:

myEvilQuery :: (MonadIO m, MonadLogger m) => SqlWriteT m ()
myEvilQuery = 
  select (return nowWithInjection) >>= liftIO . print
    nowWithInjection :: SqlExpr (Value UTCTime) 
    nowWithInjection = unsafeSqlFunction "0; DROP TABLE bar; select now" ([] :: [SqlExpr (Value Int)])

which generates this SQL:

SELECT 0; DROP TABLE bar; select now()

If we were to run the above code we would see the postgres time printed but the table bar will be erased with no indication whatsoever. Another example of this behavior is seen when using unsafeSqlValue:

myEvilQuery :: (MonadIO m, MonadLogger m) => SqlWriteT m ()
myEvilQuery = 
  select (return $ dayPart dateWithInjection) >>= liftIO . print
    dayPart :: SqlExpr (Value UTCTime) -> SqlExpr (Value Int) 
    dayPart s = unsafeSqlFunction "date_part" (unsafeSqlValue "\'day\'" :: SqlExpr (Value String) ,s)
    dateWithInjection :: SqlExpr (Value UTCTime)
    dateWithInjection = unsafeSqlValue "TIMESTAMP \'2001-02-16 20:38:40\');DROP TABLE bar; select (16"

which generates this SQL:

SELECT date_part('day', TIMESTAMP '2001-02-16 20:38:40');DROP TABLE bar; select (16)

This will print 16 and also erase the bar table. The main take away of this examples is to never use any user or third party input inside an unsafe function without first parsing it or heavily sanitizing the input.

Tests and Postgres

To run the tests, do stack test. This tests all the backends, so you’ll need to have MySQL and Postgresql installed.

Using apt-get, you should be able to do:

sudo apt-get install postgresql postgresql-contrib
sudo apt-get install libpq-dev

Using homebrew on OSx

brew install postgresql
brew install libpq

Detailed instructions on the Postgres wiki here

The connection details are located near the bottom of the test/PostgreSQL/Test.hs file:

withConn =
  R.runResourceT . withPostgresqlConn "host=localhost port=5432 user=esqutest password=esqutest dbname=esqutest"

You can change these if you like but to just get them working set up as follows on linux:

$ sudo -u postgres createuser esqutest

$ sudo -u postgres createdb esqutest

$ sudo -u postgres psql
postgres=# \password esqutest

And on osx

$ createuser esqutest

$ createdb esqutest

$ psql postgres
postgres=# \password esqutest


  • @parsonsmatt
    • #170 Add documentation to groupBy to explain tuple nesting.


  • @charukiewicz, @belevy, @joemalin95
    • #167: Exposed functions that were added in 3.3.0


  • @charukiewicz, @belevy, @joemalin95
    • #166: Add several common SQL string functions: upper_, trim_, ltrim_, rtrim_, length_, left_, right_


  • @hdgarrood
    • #163: Allow unsafeSqlFunction to take up to 10 arguments without needing to nest tuples.


  • @parsonsmatt
    • #161: Fix an issue where nested joins didn’t get the right on clause.


  • @parsonsmatt
    • #159: Add an instance of UnsafeSqlFunction () for 0-argument SQL functions.


  • @parsonsmatt
    • #153: Deprecate sub_select and introduce subSelect, subSelectMaybe, and subSelectUnsafe.
  • @parsonsmatt
    • #156: Remove the restriction that on clauses must appear in reverse order to the joining tables.


  • @JoseD92
    • #155: Added insertSelectWithConflict postgres function.


  • @tippenein
    • #149: Added associateJoin query helpers.


  • @JoseD92

    • #149: Added upsert support.
  • @parsonsmatt

    • #133: Added renderQueryToText and related functions.


  • @Vlix
    • #128: Added Database.Esqueleto.PostgreSQL.JSON module with JSON operators and JSONB data type.
  • @ibarrae
    • #127: Added between and support for composite keys in unsafeSqlBinOp.


  • @parsonsmatt
    • #122: Support persistent-2.10.0. This is a breaking change due to the removal of deprecated exports from the persistent library.
    • #113: Remove the esqueleto type class. To migrate here, use SqlExpr, SqlQuery, and SqlBackend instead of using the polymorphic Esqueleto sqlExpr sqlQuery sqlBackend => ... types.


  • @parsonsmatt
    • #117: Removed sqlQQ and executeQQ functions from export, fixing doc build and building with persistent >= 2.9


  • @ChrisCoffey
    • #114: Fix Haddock by working around an upstream bug.