on 3d0f721 help sign in
squeal/README.md
164 lines 6.8 kB history raw
1# squeal
3![squeal-icon](http://www.emoticonswallpapers.com/emotion/cute-big-pig/cute-pig-smiley-046.gif)
5[![CircleCI](https://circleci.com/gh/echatav/squeal.svg?style=svg&circle-token=a699a654ef50db2c3744fb039cf2087c484d1226)](https://circleci.com/gh/echatav/squeal)
7[Github](https://github.com/morphismtech/squeal)
9[Hackage](https://hackage.haskell.org/package/squeal-postgresql)
11[Stackage](https://www.stackage.org/package/squeal-postgresql)
13## installation
15`stack install squeal-postgresql`
17## usage
19Squeal is a deep embedding of PostgreSQL in Haskell. Let's see an example!
21First, we need some language extensions because Squeal uses modern GHC features.
23```haskell
24{-# LANGUAGE
25 DataKinds
26 , DeriveGeneric
27 , OverloadedLabels
28 , OverloadedStrings
29 , TypeApplications
30 , TypeOperators
31#-}
32```
34Here comes the Main module and imports.
36```haskell
37module Main (main) where
39import Control.Monad.Base (liftBase)
40import Data.Int (Int32)
41import Data.Text (Text)
43import Squeal.PostgreSQL
44```
46We'll use generics to easily convert between Haskell and PostgreSQL values.
48```haskell
49import qualified Generics.SOP as SOP
50import qualified GHC.Generics as GHC
51```
53The first step is to define the schema of our database. This is where we use DataKinds and TypeOperators. The schema consists of a type-level list of tables, a `:::` pairing of a type level string or `Symbol` and a list a columns, itself a `:::` pairing of a `Symbol` and a `ColumnType`. The `ColumnType` describes the PostgreSQL type of the column as well as whether or not it may contain `NULL` and whether or not inserts and updates can use a `DEFAULT`. For our schema, we'll define two tables, a users table and an emails table.
55```haskell
56type Schema =
57 '[ "users" :::
58 '[ "id" ::: 'Optional ('NotNull 'PGint4)
59 , "name" ::: 'Required ('NotNull 'PGtext)
60 ]
61 , "emails" :::
62 '[ "id" ::: 'Optional ('NotNull 'PGint4)
63 , "user_id" ::: 'Required ('NotNull 'PGint4)
64 , "email" ::: 'Required ('Null 'PGtext)
65 ]
66 ]
67```
69Next, we'll write Definitions to set up and tear down the schema. In Squeal, a Definition is a `createTable`, `alterTable` or `dropTable` command and has two type parameters, corresponding to the schema before being run and the schema after. We can compose definitions using `>>>`. Here and in the rest of our commands we make use of overloaded labels to refer to named tables and columns in our schema.
71```haskell
72setup :: Definition '[] Schema
73setup =
74 createTable #users
75 ( serial `As` #id :*
76 (text & notNull) `As` #name :* Nil )
77 [ primaryKey (Column #id :* Nil) ]
78 >>>
79 createTable #emails
80 ( serial `As` #id :*
81 (int & notNull) `As` #user_id :*
82 text `As` #email :* Nil )
83 [ primaryKey (Column #id :* Nil)
84 , foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
85 OnDeleteCascade OnUpdateCascade ]
86```
88Notice that setup starts with an empty schema `'[]` and produces `Schema`. In our `createTable` commands we included `TableConstraint`s to define primary and foreign keys, making them somewhat complex. Our tear down `Definition` is simpler.
90```haskell
91teardown :: Definition Schema '[]
92teardown = dropTable #emails >>> dropTable #users
93```
95Next, we'll write `Manipulation`s to insert data into our two tables. A `Manipulation` is an `insertInto`, `update` or `deleteFrom` command and has three type parameters, the schema it refers to, a list of parameters it can take as input, and a list of columns it produces as output. When we insert into the users table, we will need a parameter for the name field but not for the id field. Since it's optional, we can use a default value. However, since the emails table refers to the users table, we will need to retrieve the user id that the insert generates and insert it into the emails table. Take a careful look at the type and definition of both of our inserts.
97```haskell
98insertUser :: Manipulation Schema
99 '[ 'Required ('NotNull 'PGtext)]
100 '[ "fromOnly" ::: 'Required ('NotNull 'PGint4) ]
101insertUser = insertInto #users
102 ( Values (def `As` #id :* param @1 `As` #name :* Nil) [] )
103 OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
105insertEmail :: Manipulation Schema
106 '[ 'Required ('NotNull 'PGint4), 'Required ('Null 'PGtext)] '[]
107insertEmail = insertInto #emails ( Values
108 ( def `As` #id :*
109 param @1 `As` #user_id :*
110 param @2 `As` #email :* Nil) [] )
111 OnConflictDoNothing (Returning Nil)
112```
114Next we write a `Query` to retrieve users from the database. We're not interested in the ids here, just the usernames and email addresses. We need to use an inner join to get the right result. A `Query` is like a `Manipulation` with the same kind of type parameters.
116```haskell
117getUsers :: Query Schema '[]
118 '[ "userName" ::: 'Required ('NotNull 'PGtext)
119 , "userEmail" ::: 'Required ('Null 'PGtext) ]
120getUsers = select
121 (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* Nil)
122 ( from (Table (#users `As` #u)
123 & InnerJoin (Table (#emails `As` #e))
124 (#u ! #id .== #e ! #user_id)) )
125```
127Now that we've defined the SQL side of things, we'll need a Haskell type for users. We give the type `Generic` and `HasDatatypeInfo` instances so that we can decode the rows we receive when we run getUsers. Notice that the record fields of the `User` type match the column names of `getUsers`.
129```haskell
130data User = User { userName :: Text, userEmail :: Maybe Text }
131 deriving (Show, GHC.Generic)
132instance SOP.Generic User
133instance SOP.HasDatatypeInfo User
134```
136Let's also create some users to add to the database.
138```haskell
139users :: [User]
140users =
141 [ User "Alice" (Just "alice@gmail.com")
142 , User "Bob" Nothing
143 , User "Carole" (Just "carole@hotmail.com")
144 ]
145```
147Now we can put together all the pieces into a program. The program connects to the database, sets up the schema, inserts the user data (using prepared statements as an optimization), queries the user data and prints it out and finally closes the connection. We can thread the changing schema information through by using the indexed `PQ` monad transformer and when the schema doesn't change we can use `Monad` and `MonadPQ` functionality.
149```haskell
150main :: IO ()
151main = void $
152 withConnection "host=localhost port=5432 dbname=exampledb" . runPQ $
153 define setup
154 & pqThen session
155 & thenDefine teardown
156 where
157 session = do
158 idResults <- traversePrepared insertUser (Only . userName <$> users)
159 ids <- traverse (fmap fromOnly . getRow (RowNumber 0)) idResults
160 traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
161 usersResult <- runQuery getUsers
162 usersRows <- getRows usersResult
163 liftBase $ print (usersRows :: [User])
164```