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
49* json functions and operations
51* correlated subqueries
57`stack install squeal-postgresql`
61Start postgres on localhost port `5432` and create a database named `exampledb`.
67We welcome contributors.
68Please make pull requests on the `dev` branch instead of `master`.
69The `Issues` page is a good place to communicate.
75First, we need some language extensions because Squeal uses modern GHC
79>>> :set -XDataKinds -XDeriveGeneric -XOverloadedLabels -XFlexibleContexts
80>>> :set -XOverloadedStrings -XTypeApplications -XTypeOperators -XGADTs
83We'll need some imports.
86>>> import Control.Monad.IO.Class (liftIO)
87>>> import Data.Int (Int32)
88>>> import Data.Text (Text)
89>>> import Squeal.PostgreSQL
92We'll use generics to easily convert between Haskell and PostgreSQL values.
95>>> import qualified Generics.SOP as SOP
96>>> import qualified GHC.Generics as GHC
99The first step is to define the schema of our database. This is where
100we use `DataKinds` and `TypeOperators`.
105 '[ "id" ::: 'Def :=> 'NotNull 'PGint4
106 , "name" ::: 'NoDef :=> 'NotNull 'PGtext ]
107type UsersConstraints = '[ "pk_users" ::: 'PrimaryKey '["id"] ]
109 '[ "id" ::: 'Def :=> 'NotNull 'PGint4
110 , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
111 , "email" ::: 'NoDef :=> 'Null 'PGtext ]
112type EmailsConstraints =
113 '[ "pk_emails" ::: 'PrimaryKey '["id"]
114 , "fk_user_id" ::: 'ForeignKey '["user_id"] "users" '["id"] ]
116 '[ "users" ::: 'Table (UsersConstraints :=> UsersColumns)
117 , "emails" ::: 'Table (EmailsConstraints :=> EmailsColumns) ]
118type Schemas = Public Schema
122Notice the use of type operators.
124`:::` is used to pair an alias `GHC.TypeLits.Symbol` with a `SchemumType`,
125a `TableConstraint` or a `ColumnType`. It is intended to connote Haskell's `::`
128`:=>` is used to pair `TableConstraints` with a `ColumnsType`,
129yielding a `TableType`, or to pair a `ColumnConstraint` with a `NullityType`,
130yielding a `ColumnType`. It is intended to connote Haskell's `=>` operator
132Next, we'll write `Definition`s to set up and tear down the schema. In
133Squeal, a `Definition` like `createTable`, `alterTable` or `dropTable`
134has two type parameters, corresponding to the schema
135before being run and the schema after. We can compose definitions using `>>>`.
136Here and in the rest of our commands we make use of overloaded
137labels to refer to named tables and columns in our schema.
142 setup :: Definition (Public '[]) Schemas
146 (text & notNullable) `as` #name )
147 ( primaryKey #id `as` #pk_users ) >>>
150 (int & notNullable) `as` #user_id :*
151 (text & nullable) `as` #email )
152 ( primaryKey #id `as` #pk_emails :*
153 foreignKey #user_id #users #id
154 OnDeleteCascade OnUpdateCascade `as` #fk_user_id )
158We can easily see the generated SQL is unsurprising looking.
162CREATE TABLE "users" ("id" serial, "name" text NOT NULL, CONSTRAINT "pk_users" PRIMARY KEY ("id"));
163CREATE 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);
166Notice that `setup` starts with an empty schema `'[]` and produces `Schema`.
167In our `createTable` commands we included `TableConstraint`s to define
168primary and foreign keys, making them somewhat complex. Our `teardown`
169`Definition` is simpler.
174 teardown :: Definition Schemas (Public '[])
175 teardown = dropTable #emails >>> dropTable #users
183We'll need a Haskell type for @User@s. We give the type `Generics.SOP.Generic` and
184`Generics.SOP.HasDatatypeInfo` instances so that we can encode and decode @User@s.
187>>> data User = User { userName :: Text, userEmail :: Maybe Text } deriving (Show, GHC.Generic)
188>>> instance SOP.Generic User
189>>> instance SOP.HasDatatypeInfo User
192Next, we'll write `Manipulation_`s to insert @User@s into our two tables.
193A `Manipulation_` like `insertInto`, `update` or `deleteFrom`
194has three type parameters, the schemas it refers to, input parameters
195and an output row. When
196we insert into the users table, we will need a parameter for the @name@
197field but not for the @id@ field. Since it's serial, we can use a default
198value. However, since the emails table refers to the users table, we will
199need to retrieve the user id that the insert generates and insert it into
200the emails table. We can do this in a single `Manipulation_` by using a
206 insertUser :: Manipulation_ Schemas User ()
207 insertUser = with (u `as` #u) e
209 u = insertInto #users
210 (Values_ (Default `as` #id :* Set (param @1) `as` #name))
211 OnConflictDoRaise (Returning_ (#id :* param @2 `as` #email))
212 e = insertInto_ #emails $ Select
213 (Default `as` #id :* Set (#u ! #id) `as` #user_id :* Set (#u ! #email) `as` #email)
217>>> printSQL insertUser
218WITH "u" AS (INSERT INTO "users" ("id", "name") VALUES (DEFAULT, ($1 :: text)) RETURNING "id" AS "id", ($2 :: text) AS "email") INSERT INTO "emails" ("user_id", "email") SELECT "u"."id", "u"."email" FROM "u" AS "u"
221Next we write a `Query_` to retrieve users from the database. We're not
222interested in the ids here, just the usernames and email addresses. We
223need to use an `innerJoin` to get the right result. A `Query_` is like a
224`Manipulation_` with the same kind of type parameters.
229 getUsers :: Query_ Schemas () User
231 (#u ! #name `as` #userName :* #e ! #email `as` #userEmail)
232 ( from (table (#users `as` #u)
233 & innerJoin (table (#emails `as` #e))
234 (#u ! #id .== #e ! #user_id)) )
238SELECT "u"."name" AS "userName", "e"."email" AS "userEmail" FROM "users" AS "u" INNER JOIN "emails" AS "e" ON ("u"."id" = "e"."user_id")
241Let's create some users to add to the database.
248 [ User "Alice" (Just "alice@gmail.com")
250 , User "Carole" (Just "carole@hotmail.com")
255Now we can put together all the pieces into a program. The program
256connects to the database, sets up the schema, inserts the user data
257(using prepared statements as an optimization), queries the user
258data and prints it out and finally closes the connection. We can thread
259the changing schema information through by using the indexed `PQ` monad
260transformer and when the schema doesn't change we can use `Monad` and
261`MonadPQ` functionality.
266 session :: PQ Schemas Schemas IO ()
268 _ <- traversePrepared_ insertUser users
269 usersResult <- runQuery getUsers
270 usersRows <- getRows usersResult
271 liftIO $ print (usersRows :: [User])
273 withConnection "host=localhost port=5432 dbname=exampledb" $
276 & pqThen (define teardown)
278[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