• Stars
    star
    371
  • Rank 115,103 (Top 3 %)
  • Language
    Haskell
  • License
    BSD 3-Clause "New...
  • Created almost 8 years ago
  • Updated 5 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

New home of Esqueleto, please file issues so we can get things caught up!

Esqueleto CI

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.

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.

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.

Goals

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

Introduction

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

Select

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:

SELECT *
FROM Person

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

Where

Filtering by PersonName:

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

which generates this SQL:

SELECT *
FROM Person
WHERE Person.name = "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:

SELECT *
FROM Person
WHERE Person.age >= 18

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

Alternative Field Projections

The (^.) operator works on an EntityField value, which are generated by persistent as the table name + the field name. This can get a little bit verbose. As of persistent-2.11, you can use OverloadedLabels to make this a bit more concise:

{-# LANGUAGE OverloadedLabels #-}

select $ do
    p <- from $ table @Person
    pure
        ( p ^. PersonName
        , p ^. #name
        )

The OverloadedLabels support uses the fieldName as given by the Persistent entity definition syntax - no type name prefix necessary. Additionally, these field accesses are polymorphic - the following query filters any table that has a name column:

rowsByName
    :: forall rec.
    ( PersistEntity rec
    , PersistEntityBackend rec ~ SqlBackend
    , SymbolToField "name" rec Text
    )
    => SqlExpr (Value Text)
    -> SqlQuery (SqlExpr (Entity rec))
rowsByName name = do
    rec <- from $ table @rec
    where_ $ rec ^. #name ==. name
    pure rec

GHC 9.2 introduces the OverloadedRecordDot language extension, and esqueleto supports this on SqlExpr (Entity rec) and SqlExpr (Maybe (Entity rec)). It looks like this:

select $ do
    (person, blogPost) <-
        from $
            table @Person
            `leftJoin` table @BlogPost
            `on` do
                \(person :& blogPost) ->
                    just person.id ==. blogPost.authorId
    pure (person.name, blogPost.title)

Experimental/New Joins

There's a new way to write JOINs in esqueleto! It has less potential for runtime errors and is much more powerful than the old syntax. To opt in to the new syntax, import:

import Database.Esqueleto.Experimental

This will conflict with the definition of from and on in the Database.Esqueleto module, so you'll want to remove that import.

This style will become the new "default" in esqueleto-4.0.0.0, so it's a good idea to port your code to using it soon.

The module documentation in Database.Esqueleto.Experimental has many examples, and they won't be repeated here. Here's a quick sample:

select $ do
  (a :& b) <-
    from $
      Table @BlogPost
      `InnerJoin`
      Table @Person
        `on` do \(bp :& a) ->
          bp ^. BlogPostAuthorId ==. a ^. PersonId
  pure (a, b)

Advantages:

  • ON clause is attached directly to the relevant join, so you never need to worry about how they're ordered, nor will you ever run into bugs where the on clause is on the wrong JOIN
  • The ON clause lambda will all the available tables in it. This forbids runtime errors where an ON clause refers to a table that isn't in scope yet.
  • You can join on a table twice, and the aliases work out fine with the ON clause.
  • You can use UNION, EXCEPT, INTERSECTION etc with this new syntax!
  • You can reuse subqueries more easily.

Legacy Joins

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 = Person.id
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.*
FROM Person LEFT OUTER JOIN BlogPost
ON Person.id = BlogPost.authorId
ORDER BY Person.name 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 P1.id = Follow.follower
INNER JOIN Person AS P2 ON P2.id = Follow.followed

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:

INSERT INTO BlogPost
SELECT ('Group Blog Post', id)
FROM Person

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

Re-exports

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"
  where
    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"
  where
    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"
  where
    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
  where
    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
  where
    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

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

Postgres

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

MySQL

To test MySQL, you'll need to have a MySQL server installation. Then, you'll need to create a database esqutest and a 'travis'@'localhost' user which can access it:

mysql> CREATE DATABASE esqutest;
mysql> CREATE USER 'travis'@'localhost';
mysql> ALTER USER 'travis'@'localhost' IDENTIFIED BY 'esqutest';
mysql> GRANT ALL ON esqutest.* TO 'travis'@'localhost';

More Repositories

1

learnhaskell

Learn Haskell
Makefile
7,847
star
2

fp-course

Fork of the original Data61 course to be more Stack friendly
Haskell
437
star
3

bloodhound

Haskell Elasticsearch client and query DSL
Haskell
419
star
4

dotfiles

My .emacs, .screenrc, etc.
Emacs Lisp
149
star
5

revise

RethinkDB client for Clojure
Clojure
146
star
6

brambling

Datomic schema and data migration library/toolkit
Clojure
69
star
7

blackwater

Clojure SQL query logging
Clojure
56
star
8

open-haskell

Community edited and directed course based on Spring '13 cis194.
Haskell
46
star
9

blacktip

Haskell clone of Boundary's k-ordered unique id service
Haskell
40
star
10

papers

29
star
11

brotli2-rs

Brotli encoders/decoers for Rust
Rust
28
star
12

presentations

what it says on the tin
Haskell
24
star
13

hedgehog-checkers

Like the checkers library, but for hedgehog. Common stuff you'd want to check.
Haskell
22
star
14

makefiles

I use Makefile as a sort of command dispatcher/secondary memory, this is a repo of the common ones I keep reusing
17
star
15

trajectile

Tracing library for Clojure
Clojure
13
star
16

ledgertheory

Coq
13
star
17

boxcar-willie

Tomatoes or something
Rust
13
star
18

neubite

http://www.bitemyapp.com
JavaScript
11
star
19

lefortovo

Π±Π΅Π·Π³Ρ€Π°ΠΌΠΎΡ‚Π½Ρ‹ΠΉ gitignore and makefile fetcher in Rust
Rust
11
star
20

clojure-template-benchmarks

Needed proper benchmarking of frontend views
Clojure
11
star
21

bulwark

Throttling / IP banning library for Ring-compatible Clojure apps based on Kickstarter's Rack::attack
Clojure
11
star
22

monad-transformers-step-by-step

Haskell
10
star
23

bluetick

SQL DSL
Haskell
10
star
24

hacker-type-emacs

Hacker typer for Emacs
Emacs Lisp
10
star
25

buttress

Messing around
Haskell
9
star
26

shawty-prime

url shortener
Haskell
9
star
27

duke

Elasticsearch client and query DSL for Rust, based on Bloodhound
Rust
8
star
28

teef

Hakyll project for my website.
Haskell
8
star
29

studyhaskell

Study Haskell in a group setting
6
star
30

bedrock

A Yesodian foundation
Haskell
6
star
31

haskell-wishlist

Libraries wishlist / project ideas / learning opportunities
6
star
32

emojicon-bootstrap

emojicons scrape and curated database for dash expander
Python
5
star
33

persistent-activerecord-ecto

Haskell
5
star
34

my-limes

How am I going to hold onto all these mutuals, Rust edition
Rust
5
star
35

Pijul

Copy
OCaml
5
star
36

yesod-template-project

Haskell
5
star
37

shiftrss

Rust clone of https://siftrss.com/
Rust
5
star
38

lambdadelta

Imageboard software written in Haskell
Haskell
4
star
39

shawty

Tiny URL shortener web app made with Haskell, Scotty, Hedis (Redis)
Haskell
4
star
40

ghc-gc-tune

Haskell
4
star
41

styles

Stylish themes for various websites I've modified.
CSS
4
star
42

doc-workshop

Little project for automatically rebuilding documents as you save them and work on them.
Haskell
3
star
43

haskell-jwt

JWT thingy, not mine.
Haskell
3
star
44

sendgrid-haskell

Haskell client for sending email via SendGrid's API
Haskell
3
star
45

hagglers

Haskell Kaggle Project
3
star
46

monohaskell

sssshhhh
CSS
3
star
47

netwire

Real repo at: darcs get http://hub.darcs.net/ertes/netwire
Haskell
3
star
48

entr

Not mine, this is a mirror of https://bitbucket.org/eradman/entr/
C
3
star
49

github-dark-theme

my modification of the original dark userstyle to make it more readable
3
star
50

cis194-fall16

Haskell
2
star
51

furp

Every kid wants a Furpy
Haskell
2
star
52

csvtest

Haskell
2
star
53

aws-tools

Collection of tools for AWS automations
Haskell
2
star
54

polyparse

Archived from the darcs repo
Haskell
2
star
55

chat

chat
Haskell
2
star
56

grom

grom eats your Clojure code
Haskell
2
star
57

geesthacht

DynamoDB client for Haskell
Haskell
2
star
58

simon-speck-rs

Currently buggy attempt at a SIMON implementation in Rust
Rust
2
star
59

Scroot

Django auditing app for the lazy
Python
2
star
60

hpp

A Haskell Pre-Processor
Haskell
2
star
61

strong-types-and-testing

Haskell
2
star
62

hsnippet

Original is located at: https://bitbucket.org/mightybyte/hsnippet/
Haskell
2
star
63

openbusiness

contracts, etc.
2
star
64

freki

A Haskell AI suite for playing Warmachine
Haskell
2
star
65

knob

A Toggl client for posting time entries
Rust
2
star
66

timesheet-csv-example

Haskell CSV processing example
Haskell
2
star
67

whostalkin

whostalkin
Haskell
2
star
68

opvault

1Password library in Haskell
Haskell
2
star
69

ipython

same as the original, but ignores lines with #PDBNULL in them.
Python
1
star
70

stm-chans

Mirror of http://community.haskell.org/~wren/stm-chans/
Haskell
1
star
71

hickey

A git-backed wiki written in Haskell.
Haskell
1
star
72

esqueleto-select-source-error-misuse

Reproducing https://github.com/bitemyapp/esqueleto/issues/29
Haskell
1
star
73

hips

Haskell
1
star
74

hst

Automatically exported from code.google.com/p/hst
Haskell
1
star
75

exference-exference-core

Haskell
1
star
76

diwali

Little toy problem, ignore it if I haven't pointed you here
Haskell
1
star
77

example-haskell-mailer

Haskell
1
star
78

thodol

it's a wiki
JavaScript
1
star
79

plow-email

An email surver! Hooray!
Haskell
1
star
80

bluster

Tiny Clojure utility belt for SwaggerUI (http://swagger.wordnik.com/)
Clojure
1
star
81

haizod

An IRC bot on a Raspberry Pi 2
Haskell
1
star
82

luatex-arabic-example

TeX
1
star
83

Luna

The Luna Programming Language
Haskell
1
star
84

break-unagi-chan

This isn't really a "fault" in unagi-chan, I'm misusing it here.
Haskell
1
star
85

bushfire

Terminal Twitter client
Haskell
1
star
86

prelim

Better Prelude
Haskell
1
star
87

broadcast-bench

Haskell
1
star
88

rebase_example

Haskell
1
star
89

hhh

Messing around with Servant
Haskell
1
star
90

panic-repro-initTc-unsolved-constraints

Haskell
1
star
91

pinbin

Haskell
1
star
92

exference-exference

Haskell
1
star
93

Pasterip

Rips content from Pastebin, not originally mine.
Python
1
star
94

j

J Programming language source
C
1
star
95

tf-idf-1

Document ranking using tf-idf
Python
1
star
96

garrulous

chat server yo
Haskell
1
star
97

scratch

Scratch space for ideas, lessons, etc
Haskell
1
star
98

nt-in-haskell

Not mine, this is Compall's shindig - https://gitorious.org/nt-in-haskell
Haskell
1
star
99

piebot

Haskell
1
star
100

bluepencil

DraftJS raw content representation -> HTML. _Not_ licensed free or open source software.
Haskell
1
star