3
5[](https://circleci.com/gh/morphismtech/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[YouTube](https://www.youtube.com/watch?v=rWfEQfAaNc4)
17Squeal is a deep embedding of SQL into Haskell. By "deep embedding", I am abusing the
18term somewhat. What I mean is that Squeal embeds both SQL terms and SQL types
19into Haskell at the term and type levels respectively. This leads to a very high level
20of type-safety in Squeal.
22Squeal embeds not just the structured query language of SQL but also the
23data manipulation language and the data definition language; that's `SELECT`,
24`INSERT`, `UPDATE`, `DELETE`, `WITH`, `CREATE`, `DROP`, and `ALTER` commands.
26Squeal expressions closely match their corresponding SQL expressions so that
27the SQL they actually generate is completely predictable. They are also highly
28composable and cover a large portion of SQL.
32* generic encoding of Haskell tuples and records into query parameters
33 and generic decoding of query results into Haskell records
34 using [`generics-sop`](https://hackage.haskell.org/package/generics-sop)
35* access to SQL alias system using the `OverloadedLabels` extension
36* type-safe `NULL` and `DEFAULT`
37* type-safe SQL constraints `CHECK`, `UNIQUE`, `PRIMARY KEY` and `FOREIGN KEY`
38* type-safe aggregation
39* escape hatches for writing raw SQL
40* [`mtl`](https://hackage.haskell.org/package/mtl) compatible monad transformer
41 for executing as well as preparing queries and manipulations
42 and [Atkey](https://bentnib.org/paramnotions-jfp.pdf) indexed monad transformer
43 for executing definitions.
44* linear, invertible migrations
48* array, composite and enumerated types
53`stack install squeal-postgresql`
57Start postgres on localhost port `5432` and create a database named `exampledb`.
63We welcome contributors.
64Please make pull requests on the `dev` branch instead of `master`.
65The `Issues` page is a good place to communicate.
71First, we need some language extensions because Squeal uses modern GHC
75>>> :set -XDataKinds -XDeriveGeneric -XOverloadedLabels
76>>> :set -XOverloadedStrings -XTypeApplications -XTypeOperators
79We'll need some imports.
82>>> import Control.Monad (void)
83>>> import Control.Monad.IO.Class (liftIO)
84>>> import Data.Int (Int32)
85>>> import Data.Text (Text)
86>>> import Squeal.PostgreSQL
87>>> import Squeal.PostgreSQL.Render
90We'll use generics to easily convert between Haskell and PostgreSQL values.
93>>> import qualified Generics.SOP as SOP
94>>> import qualified GHC.Generics as GHC
97The first step is to define the schema of our database. This is where
98we use `DataKinds` and `TypeOperators`.
103 '[ "users" ::: 'Table (
104 '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
105 '[ "id" ::: 'Def :=> 'NotNull 'PGint4
106 , "name" ::: 'NoDef :=> 'NotNull 'PGtext
108 , "emails" ::: 'Table (
109 '[ "pk_emails" ::: 'PrimaryKey '["id"]
110 , "fk_user_id" ::: 'ForeignKey '["user_id"] "users" '["id"]
112 '[ "id" ::: 'Def :=> 'NotNull 'PGint4
113 , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
114 , "email" ::: 'NoDef :=> 'Null 'PGtext
120Notice the use of type operators.
122`:::` is used to pair an alias `GHC.TypeLits.Symbol` with a `SchemumType`,
123a `TableConstraint` or a `ColumnType`. It is intended to connote Haskell's `::`
126`:=>` is used to pair `TableConstraints` with a `ColumnsType`,
127yielding a `TableType`, or to pair a `ColumnConstraint` with a `NullityType`,
128yielding a `ColumnType`. It is intended to connote Haskell's `=>` operator
130Next, we'll write `Definition`s to set up and tear down the schema. In
131Squeal, a `Definition` like `createTable`, `alterTable` or `dropTable`
132has two type parameters, corresponding to the schema
133before being run and the schema after. We can compose definitions using `>>>`.
134Here and in the rest of our commands we make use of overloaded
135labels to refer to named tables and columns in our schema.
140 setup :: Definition '[] Schema
144 (text & notNullable) `as` #name )
145 ( primaryKey #id `as` #pk_users ) >>>
148 (int & notNullable) `as` #user_id :*
149 (text & nullable) `as` #email )
150 ( primaryKey #id `as` #pk_emails :*
151 foreignKey #user_id #users #id
152 OnDeleteCascade OnUpdateCascade `as` #fk_user_id )
156We can easily see the generated SQL is unsurprising looking.
160CREATE TABLE "users" ("id" serial, "name" text NOT NULL, CONSTRAINT "pk_users" PRIMARY KEY ("id"));
161CREATE TABLE "emails" ("id" serial, "user_id" int NOT NULL, "email" text NULL, CONSTRAINT "pk_emails" PRIMARY KEY ("id"), CONSTRAINT "fk_user_id" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE);
164Notice that `setup` starts with an empty schema `'[]` and produces `Schema`.
165In our `createTable` commands we included `TableConstraint`s to define
166primary and foreign keys, making them somewhat complex. Our `teardown`
167`Definition` is simpler.
172 teardown :: Definition Schema '[]
173 teardown = dropTable #emails >>> dropTable #users
181Next, we'll write `Manipulation`s to insert data into our two tables.
182A `Manipulation` like `insertRow`, `update` or `deleteFrom`
183has three type parameters, the schema it refers to, a list of parameters
184it can take as input, and a list of columns it produces as output. When
185we insert into the users table, we will need a parameter for the `name`
186field but not for the `id` field. Since it's serial, we can use a default
187value. However, since the emails table refers to the users table, we will
188need to retrieve the user id that the insert generates and insert it into
189the emails table. Take a careful look at the type and definition of both
195 insertUser :: Manipulation Schema '[ 'NotNull 'PGtext ] '[ "fromOnly" ::: 'NotNull 'PGint4 ]
196 insertUser = insertRow #users
197 (Default `as` #id :* Set (param @1) `as` #name)
198 OnConflictDoNothing (Returning (#id `as` #fromOnly))
203 insertEmail :: Manipulation Schema '[ 'NotNull 'PGint4, 'Null 'PGtext] '[]
204 insertEmail = insertRow #emails
205 ( Default `as` #id :*
206 Set (param @1) `as` #user_id :*
207 Set (param @2) `as` #email )
208 OnConflictDoNothing (Returning Nil)
211>>> printSQL insertUser
212INSERT INTO "users" ("id", "name") VALUES (DEFAULT, ($1 :: text)) ON CONFLICT DO NOTHING RETURNING "id" AS "fromOnly"
213>>> printSQL insertEmail
214INSERT INTO "emails" ("id", "user_id", "email") VALUES (DEFAULT, ($1 :: int4), ($2 :: text)) ON CONFLICT DO NOTHING
217Next we write a `Query` to retrieve users from the database. We're not
218interested in the ids here, just the usernames and email addresses. We
219need to use an inner join to get the right result. A `Query` is like a
220`Manipulation` with the same kind of type parameters.
225 getUsers :: Query Schema '[]
226 '[ "userName" ::: 'NotNull 'PGtext
227 , "userEmail" ::: 'Null 'PGtext ]
229 (#u ! #name `as` #userName :* #e ! #email `as` #userEmail)
230 ( from (table (#users `as` #u)
231 & innerJoin (table (#emails `as` #e))
232 (#u ! #id .== #e ! #user_id)) )
236SELECT "u"."name" AS "userName", "e"."email" AS "userEmail" FROM "users" AS "u" INNER JOIN "emails" AS "e" ON ("u"."id" = "e"."user_id")
239Now that we've defined the SQL side of things, we'll need a Haskell type
240for users. We give the type `Generics.SOP.Generic` and
241`Generics.SOP.HasDatatypeInfo` instances so that we can decode the rows
242we receive when we run `getUsers`. Notice that the record fields of the
243`User` type match the column names of `getUsers`.
246>>> data User = User { userName :: Text, userEmail :: Maybe Text } deriving (Show, GHC.Generic)
247>>> instance SOP.Generic User
248>>> instance SOP.HasDatatypeInfo User
251Let's also create some users to add to the database.
258 [ User "Alice" (Just "alice@gmail.com")
260 , User "Carole" (Just "carole@hotmail.com")
265Now we can put together all the pieces into a program. The program
266connects to the database, sets up the schema, inserts the user data
267(using prepared statements as an optimization), queries the user
268data and prints it out and finally closes the connection. We can thread
269the changing schema information through by using the indexed `PQ` monad
270transformer and when the schema doesn't change we can use `Monad` and
271`MonadPQ` functionality.
276 session :: PQ Schema Schema IO ()
278 idResults <- traversePrepared insertUser (Only . userName <$> users)
279 ids <- traverse (fmap fromOnly . getRow 0) idResults
280 traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
281 usersResult <- runQuery getUsers
282 usersRows <- getRows usersResult
283 liftIO $ print (usersRows :: [User])
285 void . withConnection "host=localhost port=5432 dbname=exampledb" $
288 & pqThen (define teardown)
290[User {userName = "Alice", userEmail = Just "alice@gmail.com"},User {userName = "Bob", userEmail = Nothing},User {userName = "Carole", userEmail = Just "carole@hotmail.com"}]
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list