@@ -17,148 +17,223 @@
19Squeal is a deep embedding of PostgreSQL in Haskell. Let's see an example!
19Squeal is a deep embedding of PostgreSQL in Haskell. Let's see an example!
21−First, we need some language extensions because Squeal uses modern GHC features.
20+First, we need some language extensions because Squeal uses modern GHC
24−{-# LANGUAGE
23+>>> :set -XDataKinds -XDeriveGeneric -XOverloadedLabels
25− DataKinds
24+>>> :set -XOverloadedStrings -XTypeApplications -XTypeOperators
34−Here comes the Main module and imports.
27+We'll need some imports.
37−module Main (main) where
30+>>> import Control.Monad (void)
39−import Control.Monad.Base (liftBase)
31+>>> import Control.Monad.Base (liftBase)
40−import Data.Int (Int32)
32+>>> import Data.Int (Int32)
41−import Data.Text (Text)
33+>>> import Data.Text (Text)
43−import Squeal.PostgreSQL
34+>>> import Squeal.PostgreSQL
46We'll use generics to easily convert between Haskell and PostgreSQL values.
37We'll use generics to easily convert between Haskell and PostgreSQL values.
49−import qualified Generics.SOP as SOP
40+>>> import qualified Generics.SOP as SOP
50−import qualified GHC.Generics as GHC
41+>>> import qualified GHC.Generics as GHC
53−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.
44+The first step is to define the schema of our database. This is where
45+we use @DataKinds@ and @TypeOperators@. The schema consists of a type-level
46+list of tables, a `:::` pairing of a type level string or
47+@Symbol@ and a list a columns, itself a `:::` pairing of a
48+@Symbol@ and a `ColumnType`. The `ColumnType` describes the
49+PostgreSQL type of the column as well as whether or not it may contain
50+@NULL@ and whether or not inserts and updates can use a @DEFAULT@. For our
51+schema, we'll define two tables, a users table and an emails table.
56type Schema =
55type Schema =
57− '[ "users" :::
56+ '[ "users" :::
57+ '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
58− '[ "id" ::: 'Optional ('NotNull 'PGint4)
58+ '[ "id" ::: 'Def :=> 'NotNull 'PGint4
59− , "name" ::: 'Required ('NotNull 'PGtext)
59+ , "name" ::: 'NoDef :=> 'NotNull 'PGtext
61 , "emails" :::
61 , "emails" :::
62+ '[ "pk_emails" ::: 'PrimaryKey '["id"]
63+ , "fk_user_id" ::: 'ForeignKey '["user_id"] "users" '["id"]
62− '[ "id" ::: 'Optional ('NotNull 'PGint4)
65+ '[ "id" ::: 'Def :=> 'NotNull 'PGint4
63− , "user_id" ::: 'Required ('NotNull 'PGint4)
66+ , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
64− , "email" ::: 'Required ('Null 'PGtext)
67+ , "email" ::: 'NoDef :=> 'Null 'PGtext
69−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.
73+Next, we'll write `Definition`s to set up and tear down the schema. In
74+Squeal, a `Definition` is a `createTable`, `alterTable` or `dropTable`
75+command and has two type parameters, corresponding to the schema
76+before being run and the schema after. We can compose definitions using
77+`>>>`. Here and in the rest of our commands we make use of overloaded
78+labels to refer to named tables and columns in our schema.
72−setup :: Definition '[] Schema
83+ setup :: Definition '[] Schema
74− createTable #users
85+ createTable #users
75− ( serial `As` #id :*
86+ ( serial `As` #id :*
76− (text & notNull) `As` #name :* Nil )
87+ (text & notNull) `As` #name :* Nil )
77− [ primaryKey (Column #id :* Nil) ]
88+ ( primaryKey (Column #id :* Nil) `As` #pk_users :* Nil ) >>>
79− createTable #emails
89+ createTable #emails
80− ( serial `As` #id :*
90+ ( serial `As` #id :*
81− (int & notNull) `As` #user_id :*
91+ (int & notNull) `As` #user_id :*
82− text `As` #email :* Nil )
92+ text `As` #email :* Nil )
83− [ primaryKey (Column #id :* Nil)
93+ ( primaryKey (Column #id :* Nil) `As` #pk_emails :*
84− , foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
94+ foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
85− OnDeleteCascade OnUpdateCascade ]
95+ OnDeleteCascade OnUpdateCascade `As` #fk_user_id :* Nil )
88−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.
98+
99+We can easily see the generated SQL is unsuprising looking.
90```haskell
101```haskell
91−teardown :: Definition Schema '[]
102+>>> renderDefinition setup
92−teardown = dropTable #emails >>> dropTable #users
103+```
105+"CREATE TABLE users (id serial, name text NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id)); CREATE TABLE emails (id serial, user_id int NOT NULL, email text, CONSTRAINT pk_emails PRIMARY KEY (id), CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES rs (id) ON DELETE CASCADE ON UPDATE CASCADE);"
95−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.
108+Notice that @setup@ starts with an empty schema @'[]@ and produces @Schema@.
109+In our `createTable` commands we included `TableConstraint`s to define
110+primary and foreign keys, making them somewhat complex. Our tear down
111+`Definition` is simpler.
97```haskell
113```haskell
98−insertUser :: Manipulation Schema
114+>>> :{
99− '[ 'Required ('NotNull 'PGtext)]
115+let
100− '[ "fromOnly" ::: 'Required ('NotNull 'PGint4) ]
116+ teardown :: Definition Schema '[]
101−insertUser = insertInto #users
117+ teardown = dropTable #emails >>> dropTable #users
102− ( Values (def `As` #id :* param @1 `As` #name :* Nil) [] )
118+:}
103− OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
119+>>> renderDefinition teardown
105−insertEmail :: Manipulation Schema
121+```sql
106− '[ 'Required ('NotNull 'PGint4), 'Required ('Null 'PGtext)] '[]
122+"DROP TABLE emails; DROP TABLE users;"
107−insertEmail = insertInto #emails ( Values
109− param @1 `As` #user_id :*
110− param @2 `As` #email :* Nil) [] )
111− OnConflictDoNothing (Returning Nil)
114−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.
125+Next, we'll write `Manipulation`s to insert data into our two tables.
126+A `Manipulation` is a `insertInto`, `update` or `deleteFrom` command and
127+has three type parameters, the schema it refers to, a list of parameters
128+it can take as input, and a list of columns it produces as output. When
129+we insert into the users table, we will need a parameter for the @name@
130+field but not for the @id@ field. Since it's optional, we can use a default
131+value. However, since the emails table refers to the users table, we will
132+need to retrieve the user id that the insert generates and insert it into
133+the emails table. Take a careful look at the type and definition of both
116```haskell
136```haskell
117−getUsers :: Query Schema '[]
137+>>> :{
118− '[ "userName" ::: 'Required ('NotNull 'PGtext)
138+let
119− , "userEmail" ::: 'Required ('Null 'PGtext) ]
139+ insertUser :: Manipulation Schema '[ 'NotNull 'PGtext ]
120−getUsers = select
140+ '[ "fromOnly" ::: 'NotNull 'PGint4 ]
121− (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* Nil)
141+ insertUser = insertRow #users
122− ( from (Table (#users `As` #u)
142+ (Default `As` #id :* Set (param @1) `As` #name :* Nil)
123− & InnerJoin (Table (#emails `As` #e))
143+ OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
124− (#u ! #id .== #e ! #user_id)) )
144+:}
147+ insertEmail :: Manipulation Schema '[ 'NotNull 'PGint4, 'Null 'PGtext] '[]
148+ insertEmail = insertRow #emails
149+ ( Default `As` #id :*
150+ Set (param @1) `As` #user_id :*
151+ Set (param @2) `As` #email :* Nil )
152+ OnConflictDoNothing (Returning Nil)
154+>>> renderManipulation insertUser
155+"INSERT INTO users (id, name) VALUES (DEFAULT, ($1 :: text)) ON CONFLICT DO NOTHING URNING id AS fromOnly;"
156+>>> renderManipulation insertEmail
159+"INSERT INTO emails (id, user_id, email) VALUES (DEFAULT, ($1 :: int4), ($2 :: text)N CONFLICT DO NOTHING;"
127−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`.
162+Next we write a `Query` to retrieve users from the database. We're not
163+interested in the ids here, just the usernames and email addresses. We
164+need to use an inner join to get the right result. A `Query` is like a
165+`Manipulation` with the same kind of type parameters.
129```haskell
167```haskell
170+ getUsers :: Query Schema '[]
171+ '[ "userName" ::: 'NotNull 'PGtext
172+ , "userEmail" ::: 'Null 'PGtext ]
174+ (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* Nil)
175+ ( from (table (#users `As` #u)
176+ & innerJoin (table (#emails `As` #e))
177+ (#u ! #id .== #e ! #user_id)) )
179+>>> renderQuery getUsers
182+"SELECT u.name AS userName, e.email AS userEmail FROM users AS u INNER JOIN emails e ON (u.id = e.user_id)"
184+Now that we've defined the SQL side of things, we'll need a Haskell type
185+for users. We give the type `Generics.SOP.Generic` and
186+`Generics.SOP.HasDatatypeInfo` instances so that we can decode the rows
187+we receive when we run @getUsers@. Notice that the record fields of the
188+@User@ type match the column names of @getUsers@.
130−data User = User { userName :: Text, userEmail :: Maybe Text }
191+>>> data User = User { userName :: Text, userEmail :: Maybe Text } deriving (Show, .Generic)
131− deriving (Show, GHC.Generic)
132−instance SOP.Generic User
192+>>> instance SOP.Generic User
133−instance SOP.HasDatatypeInfo User
193+>>> instance SOP.HasDatatypeInfo User
136Let's also create some users to add to the database.
196Let's also create some users to add to the database.
138```haskell
198```haskell
139−users :: [User]
201+ users :: [User]
141− [ User "Alice" (Just "alice@gmail.com")
203+ [ User "Alice" (Just "alice@gmail.com")
142− , User "Bob" Nothing
204+ , User "Bob" Nothing
143− , User "Carole" (Just "carole@hotmail.com")
205+ , User "Carole" (Just "carole@hotmail.com")
147−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.
210+Now we can put together all the pieces into a program. The program
211+connects to the database, sets up the schema, inserts the user data
212+(using prepared statements as an optimization), queries the user
213+data and prints it out and finally closes the connection. We can thread
214+the changing schema information through by using the indexed `PQ` monad
215+transformer and when the schema doesn't change we can use `Monad` and
216+`MonadPQ` functionality.
149```haskell
218```haskell
150−main :: IO ()
219+>>> :{
152− withConnection "host=localhost port=5432 dbname=exampledb" . runPQ $
221+ session :: PQ Schema Schema IO ()
155− & thenDefine teardown
157− session = do
222+ session = do
158− idResults <- traversePrepared insertUser (Only . userName <$> users)
223+ idResults <- traversePrepared insertUser (Only . userName <$> users)
159− ids <- traverse (fmap fromOnly . getRow (RowNumber 0)) idResults
224+ ids <- traverse (fmap fromOnly . getRow (RowNumber 0)) idResults
160− traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
225+ traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
161− usersResult <- runQuery getUsers
226+ usersResult <- runQuery getUsers
162− usersRows <- getRows usersResult
227+ usersRows <- getRows usersResult
163− liftBase $ print (usersRows :: [User])
228+ liftBase $ print (usersRows :: [User])
231+void . withConnection "host=localhost port=5432 dbname=exampledb" $
234+ & thenDefine teardown
238+[User {userName = "Alice", userEmail = Just "alice@gmail.com"},User {userName = "Bob", userEmail = Nothing},User {userName = "Carole", userEmail = Just role@hotmail.com"}]