on dev help sign in
readme
commit748fab2f0b9f7a3be3e6d82567ee1c6635524b49 authorEitan Chatav <eitan.chatav@gmail.com> dateThu 28 Sep 2017, 20:11 (9 years ago) parenta9c8e46
3 files changed+155−4
README.md+1−3 squeal-postgresql/README.md+153 squeal-postgresql/squeal-postgresql.cabal+1−1
README.md+1−3
@@ -8,9 +8,7 @@ Main repository for the squeal database library.
88
99## installation
1010
11−The easiest way to install `squeal-postgresql` is to use the haskell [stack](https://docs.haskellstack.org/en/stable/README/) tool.
12−
13−Use `resolver: nightly-2017-08-25`. We're stuck with nightlies until an lts supports GHC-8.2. Do a `stack upgrade && stack update` to make sure you get GHC-8.2 and the nightly. Then add `squeal-postgresql-0.1.1.2` as an extra dependency in your `stack.yaml` and you should be good to go.
11+`stack install squeal-postgresql`
1412
1513## usage
1614
squeal-postgresql/README.md+153
@@ -5,3 +5,156 @@
55[![CircleCI](https://circleci.com/gh/echatav/squeal.svg?style=svg&circle-token=a699a654ef50db2c3744fb039cf2087c484d1226)](https://circleci.com/gh/echatav/squeal)
66
77Main repository for the squeal database library.
8+
9+## installation
10+
11+`stack install squeal-postgresql`
12+
13+## usage
14+
15+Squeal is a deep embedding of PostgreSQL in Haskell. Let's see an example!
16+
17+First, we need some language extensions because Squeal uses modern GHC features.
18+
19+```haskell
20+{-# LANGUAGE
21+ DataKinds
22+ , DeriveGeneric
23+ , OverloadedLabels
24+ , OverloadedStrings
25+ , TypeApplications
26+ , TypeOperators
27+#-}
28+```
29+
30+Here comes the Main module and imports.
31+
32+```haskell
33+module Main (main) where
34+
35+import Control.Monad.Base (liftBase)
36+import Data.Int (Int32)
37+import Data.Text (Text)
38+
39+import Squeal.PostgreSQL
40+```
41+
42+We'll use generics to easily convert between Haskell and PostgreSQL values.
43+
44+```haskell
45+import qualified Generics.SOP as SOP
46+import qualified GHC.Generics as GHC
47+```
48+
49+The 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.
50+
51+```haskell
52+type Schema =
53+ '[ "users" :::
54+ '[ "id" ::: 'Optional ('NotNull 'PGint4)
55+ , "name" ::: 'Required ('NotNull 'PGtext)
56+ ]
57+ , "emails" :::
58+ '[ "id" ::: 'Optional ('NotNull 'PGint4)
59+ , "user_id" ::: 'Required ('NotNull 'PGint4)
60+ , "email" ::: 'Required ('Null 'PGtext)
61+ ]
62+ ]
63+```
64+
65+Next, 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.
66+
67+```haskell
68+setup :: Definition '[] Schema
69+setup =
70+ createTable #users
71+ ( serial `As` #id :*
72+ (text & notNull) `As` #name :* Nil )
73+ [ primaryKey (Column #id :* Nil) ]
74+ >>>
75+ createTable #emails
76+ ( serial `As` #id :*
77+ (int & notNull) `As` #user_id :*
78+ text `As` #email :* Nil )
79+ [ primaryKey (Column #id :* Nil)
80+ , foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
81+ OnDeleteCascade OnUpdateCascade ]
82+```
83+
84+Notice 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.
85+
86+```haskell
87+teardown :: Definition Schema '[]
88+teardown = dropTable #emails >>> dropTable #users
89+```
90+
91+Next, 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.
92+
93+```haskell
94+insertUser :: Manipulation Schema
95+ '[ 'Required ('NotNull 'PGtext)]
96+ '[ "fromOnly" ::: 'Required ('NotNull 'PGint4) ]
97+insertUser = insertInto #users
98+ ( Values (def `As` #id :* param @1 `As` #name :* Nil) [] )
99+ OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
100+
101+insertEmail :: Manipulation Schema
102+ '[ 'Required ('NotNull 'PGint4), 'Required ('Null 'PGtext)] '[]
103+insertEmail = insertInto #emails ( Values
104+ ( def `As` #id :*
105+ param @1 `As` #user_id :*
106+ param @2 `As` #email :* Nil) [] )
107+ OnConflictDoNothing (Returning Nil)
108+```
109+
110+Next 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.
111+
112+```haskell
113+getUsers :: Query Schema '[]
114+ '[ "userName" ::: 'Required ('NotNull 'PGtext)
115+ , "userEmail" ::: 'Required ('Null 'PGtext) ]
116+getUsers = select
117+ (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* Nil)
118+ ( from (Table (#users `As` #u)
119+ & InnerJoin (Table (#emails `As` #e))
120+ (#u ! #id .== #e ! #user_id)) )
121+```
122+
123+Now 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`.
124+
125+```haskell
126+data User = User { userName :: Text, userEmail :: Maybe Text }
127+ deriving (Show, GHC.Generic)
128+instance SOP.Generic User
129+instance SOP.HasDatatypeInfo User
130+```
131+
132+Let's also create some users to add to the database.
133+
134+```haskell
135+users :: [User]
136+users =
137+ [ User "Alice" (Just "alice@gmail.com")
138+ , User "Bob" Nothing
139+ , User "Carole" (Just "carole@hotmail.com")
140+ ]
141+```
142+
143+Now 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.
144+
145+```haskell
146+main :: IO ()
147+main = void $
148+ withConnection "host=localhost port=5432 dbname=exampledb" . runPQ $
149+ define setup
150+ & pqThen session
151+ & thenDefine teardown
152+ where
153+ session = do
154+ idResults <- traversePrepared insertUser (Only . userName <$> users)
155+ ids <- traverse (fmap fromOnly . getRow (RowNumber 0)) idResults
156+ traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
157+ usersResult <- runQuery getUsers
158+ usersRows <- getRows usersResult
159+ liftBase $ print (usersRows :: [User])
160+```
squeal-postgresql/squeal-postgresql.cabal+1−1
@@ -12,7 +12,7 @@ copyright: Copyright (c) 2017 Morphism, LLC
1212category: Database
1313build-type: Simple
1414cabal-version: >=1.18
15−extra-doc-files: ../README.md
15+extra-doc-files: README.md
1616
1717source-repository head
1818 type: git