@@ -0,0 +1,876 @@
5+ <meta charset="utf-8">
7+ @import url(https://fonts.googleapis.com/css?family=PT+Sans+Narrow);
8+ @import url(https://fonts.googleapis.com/css?family=Montserrat:400,700,400italic);
9+ @import url(https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700,400italic);
11+ body { font-family: 'Montserrat'; }
13+ font-family: 'PT Sans Narrow';
18+ background-color: #ccc;
22+ .remark-code, .remark-inline-code { font-family: 'Ubuntu Mono'; }
34+ <textarea id="source">
41+_A bridge between SQL and Haskell_
43+.signature[2018/09/12 - by @Raveline]
49+Using `postgresql-simple` you have a typical "Trial, error, despair" workflow:
51+- Write query manually or semi-automatically
53+- Write haskell code using the query
55+- Pray that the query syntax is correct
57+- Pray that it returns what you want
59+- Realize the inefficiency of prayer when it comes to SQL
61+- Iterate till it works
68+
72+# Use your best friend: GHC
74+Squeal provides several eDSL to make your SQL typesafe:
76+- Type level eDSL to express schema;
78+- Value level eDSL to manipulate schema (plus migration, yeehaw !);
80+- Type level eDSL to express queries;
82+- Value level eDSL to perform queries
84+However, it's not an ORM. There's no caching, lazy loading - you retain control over your memory. Also, joins - and mostly aggregation after joins - have to be handled manually.
91+# Part I. Schema & migration
97+- The schema will be used to validate _everything_: migration, queries, etc.
99+- A schema is mostly a collection of tables.
101+- In this presentation, we will create a very basic database modeling a parliament. We will store _members of parliament_ and _parliamentary groups_.
103+- A simple Schema to represent a Parliament could be:
106+type Schema = '[ "mp" ::: 'Table MemberOfParliament
107+ , "groupp" ::: 'Table ParliamentGroup ]
113+We need the `DataKinds` extension to be able to express heterogenous lists containing
114+specific types like this one.
117+You can perfectly call your table "group" and not "groupp" even though it is a
118+keyword in SQL - Squeal queries will be properly escaped.
126+type ParliamentaryGroup =
127+ '[ "pk_groupp" ::: 'PrimaryKey '["groupp_id"]]
129+ '[ "groupp_id" ::: 'NoDef :=> 'NotNull 'PGuuid
130+ , "name" ::: 'NoDef :=> 'NotNull 'PGtext
134+- That's a whole simple table defined in one go.
136+- `:::` lets us define a column or a constraint.
138+- `:=>` associates the constraints to the column.
140+- Let's split constraints and column to study the syntax a bit more.
145+We are using `:::` and `:=>` to quickly express associations when writing
146+our schema. We need the `TypeOperators` extension.]
150+class: center, middle
153+
160+type MemberOfParliament =
161+ '[ "pk_mp" ::: 'PrimaryKey '["mp_id"]
162+ , "fk_mp_groupp" ::: 'ForeignKey '["mp_group"] "groupp" '["groupp_id"]
166+- A table is a collection of constraints associated to a collection of columns.
168+- We defined a table named "mp".
170+- It has a constraint named "pk_mp", defining its primary key on column "mp_id".
172+- It has a constraint named "fk_mp_groupp" defining a foreign key on column "mg_group", connected to the column "groupp_id" of table "groupp".
174+- These constraints will be associated to the columns defined in `MpCols`.
182+ '[ "mp_id" ::: 'NoDef :=> 'NotNull 'PGuuid
183+ , "first_name" ::: 'NoDef :=> 'NotNull 'PGtext
184+ , "last_name" ::: 'NoDef :=> 'NotNull 'PGtext
188+- A listing of column associate, for each element:
192+ * the mention of an eventual default value;
196+ * the type (obviously).
201+GHC is already helping. If I named the "mp_id" column differently, GHC would yell because I promised a primary key constraint on a column named `mp_id`, so there must be one.]
205+# Implementating the schema
207+- We will carry this schema type pretty much everywhere.
209+- But before we play with this schema, we need to implement it.
214+setup :: Definition '[] Schema
217+ ( notNullable uuid `as` #groupp_id
218+ :* notNullable text `as` #name)
219+ ( primaryKey #groupp_id `as` #pk_groupp )
221+ ( notNullable uuid `as` #mp_id
222+ :* notNullable text `as` #first_name
223+ :* notNullable text `as` #last_name
224+ :* notNullable text `as` #mp_group)
225+ ( primaryKey #mp_id `as` #pk_mp
226+ :* foreignKey #mp_group #groupp #groupp_id
227+ OnDeleteCascade OnUpdateCascade
233+# Damn, that's verbose
235+- Yes. But you need verbosity to get type safety.
237+- On the plus side, it's fairly straightforward.
239+- You use `:*` to compose the element of the heterogenous list of columns.
241+- You use `>>>` to compose table creation.
243+- Note that the compiler will catch any mistype between Schema and
244+definition; wrong nullability, wrong type, wrong name, etc.
249+You'll also need `OverloadedLabels`, for naming stuff.
250+This is mostly to avoid having to write manual proxies all
251+the time and for convenience.]
255+# Setting up the schema
257+- Squeal comes with a very good migration manager, handling _upgrades_ AND
260+- A migration is a simple type:
263+Migration io schema0 schema1
266+- The first parameter is a BaseMonad (typically, IO).
268+- The second parameter is "the current schema of your DB".
270+- The last one is "what you will migrate to".
272+- The `Definition` type used to define upgrade and downgrade functions use the
273+ same logic (from one schema to the other).
277+# Defining our first migration
279+- We have the setup bit, we need the teardown:
282+tearDown :: Definition Schema '[]
283+tearDown = dropTable #mp >>> dropTable #groupp
286+- Note that GHC will also detect the _proper_ order of what you typed in
287+ downgrade and upgrade should there be any conflict (with foreign keys).
290+initDB :: Migration IO '[] Schema
292+ Migration { name = "Schema creation"
293+ , up = void $ define setup
294+ , down = void $ define tearDown }
299+# Simple migrator example
305+ void $ withConnection connectionString $
306+ migrateUp $ single initDB
309+- We print the migration query; you can do this for all queries generated through Squeal.
311+- We use `migrateUp` to perform the migration.
313+- You can run several migration at the same time, to run only one use `single`.
317+class: center, middle
320+
324+class: center, middle
327+# Part II. Insertions
331+# Manipulations & Queries
333+Besides the specific case of migration, you will mostly perform:
335+- `Manipulation` : inserting, updating and deleting data.
337+- `Query` : fetching data.
339+- Both types are parametric over the same things:
347+- Simplified, they look like this:
350+Manipulation schema params columns
352+Query schema params columns
361+type Parliament = [Group]
364+ Group { name :: Text
365+ , members :: [MemberOfParliament] }
367+data MemberOfParliament =
368+ MemberOfParliament { firstName :: Text
369+ , lastName :: Text }
372+- You'll note that we didn't use anything from Squeal.
374+- The model can be entirely separated from the persistence layer.
378+# Inserting a Parliamentary Group
380+- A group is very simple: it's a uuid and a name. Let's define our params:
383+type GroupInsertionParams = '[ 'NotNull 'PGuuid
384+ , 'NotNull 'PGtext ]
387+- Params are not named, but they are indexed. You just need nullability and type.
392+groupInsertion :: Manipulation Schema GroupInsertionParams '[]
394+ insertRow_ #groupp ( Set (param @1) `as` #groupp_id
395+ :* Set (param @2) `as` #name )
400+- `TypeApplication` lets us use the index of parameters (counting from 1).
402+- Once again: all this is checked by the compiler. Wrong name, wrong type... you'll get a compile error.
404+- `insertRow_` is a simplified version of `insertRow`. `insertRow` lets you express a `RETURNING` clause and the expected behaviour in case of conflict.
408+_Inserting a Member of Parliament_
409+# Doing an INSERT INTO ... SELECT
411+- We could create a naive query that takes MP uuid, first name, last name and group uuid...
415+- But that's boring. So let's use the `INSERT INTO ... SELECT`.
419+- We will build a query that will return as constants our MP's uuid, first name and last name...
423+- ... and fetch the uuid of a group given the name of the group.
427+- There's a `insertQuery` utility function for that. All we have to do is write the `select` !
430+mpInsertion :: Manipulation Schema MpParams '[]
432+ insertQuery_ #mp selectGroup -- we just have to write selectGroup !
437+_Inserting a Member of Parliament_
440+- A query looks just like a `Manipulation`:
443+Query schema params returns
446+- Returns use a special syntax, that demands names, nullability and types:
449+type ExampleReturnType = '[ "some_column" ::: 'NotNull 'PGuuid
450+ , "other_column" ::: 'Null 'PGtext ]
453+- It is not the same as the column definition we used in the schema. That one also expects that you specify, for each column, an eventual default:
456+type ExampleSchemaCols =
457+ '[ "some_column" ::: 'NoDef :=> 'NotNull 'PGuuid
458+ , "other_column" ::: 'NoDef :=> 'Null 'PGtext ]
463+_Inserting a Member of Parliament_
464+# Don't rewrite column definitions
466+- We want our return type to be "all columns from table MP expressed as return type".
468+- But we cannot reuse our neat `MpCols` alias, since the type do not match as we've just seen.
470+- Hopefully, there's a neat Type Family that will let you convert any table you defined to the type of the equivalent row: `TableToRow`.
472+- We finally have the signature for our SELECT query.
475+selectGroup :: Query Schema MpParams (TableToRow MemberOfParliament)
480+_Inserting a Member of Parliament_
481+# Our final Select query
483+- Our intermediate query looks like this:
488+ ( param @1 `as` #mp_id
489+ :* param @2 `as` #first_name
490+ :* param @3 `as` #last_name
491+ :* #groupp ! #groupp_id `as` #mp_group
493+ ( from (table #groupp)
494+ & where_ ( #groupp ! #name .== param @4 )
498+- The first three columns are constant defined through our params;
500+- ... we alias the column name using `as`...
502+- ... and when we need the result from the table we use the `#table ! #column` syntax.
504+- We'll go back to the "from" block later.
508+# Dealing with the connection context
510+- Queries are runned in a connection context:
512+ * When dealing with a single-connection context, use the type `PQ`.
514+ * When dealing with a pool of connection, use the type `PoolPQ`.
516+- Or get rid of the context and use `mtl` style, with the typeclass `MonadPQ`.
518+- We'll use that to demonstrate how to actually run our queries.
522+# Inserting a whole group
525+insertGroup :: (MonadPQ Schema m, MonadBaseControl IO m) => Group -> m ()
526+insertGroup g@(Group name _) = do
527+ uuid' <- liftBase nextRandom
528+ void $ manipulateParams groupInsertion (uuid', name)
532+- Remember that we defined a group as a name and a list of MPs.
534+- We are in `MonadBaseControl`, so we cannot use `liftIO`, we need `liftBase`.
536+- To perform a simple insertion, use `manipulateParams`. It takes into parameter
537+instances of `ToParam`, but you will typically use tuples or Generic-SOP.
541+# Inserting a bunch of MPs
544+insertMps :: (MonadPQ Schema m, MonadBaseControl IO m) => Group -> m ()
545+insertMps (Group groupName mps) =
546+ let tuplify Mp{..} = (, firstName, lastName, groupName) <$> nextRandom
547+ params = traverse tuplify mps
548+ in void $ liftBase params >>= traversePrepared mpInsertion
551+- This time, we want to do a `preparedStatement`. We'll use `traversePrepare`
552+which behaves like `manipulateParams`, but expect a list of `ToParam`
555+- We build our tuple manually again, mostly because we want to generate UUID on
556+the fly and we don't want to have them in our model.
560+# Inserting a whole parliament
564+ (MonadPQ Schema m, MonadBaseControl IO m) => Parliament -> m ()
565+insertParliament = traverse_ insertGroup
568+- That's simple enough !
570+- If we want to be a bit safer, though, we can wrap this call in a
571+`transactionally_` function, which will put all that in a transaction.
575+class: center, middle
578+
583+class: center, middle
591+- Building basic queries is easy, and is well documented.
593+- However, the real interest of a tool like Squeal is in DRYness, and the documentation is still lacking in "how-to" related to composability.
595+- I'll build an example showing how column selections and from clause can be factorized.
597+- We want to write two queries:
599+ * One to get all members of a specific group.
601+ * One to get all the parliament.
605+# Decomposing a `Query`
607+- A query typically stars with `select` (variants are available).
611+ * A heterogenous list of fields with a scary signature;
613+ * A virtual table (the from clause and filter clauses), called a `TableExpression`.
615+- And return fields. Ours will look like this:
618+type GroupRowResult =
619+ '[ "groupName" ::: 'NotNull 'PGtext
620+ , "firstName" ::: 'NotNull 'PGtext
621+ , "lastName" ::: 'NotNull 'PGtext ]
626+# The Table Expression
628+- It necessarily contains a `fromClause` (table, view or subquery, plus optional joins).
630+- You can add where, groups, "HAVING" clause, order, etc.
632+- `from` creates a basic `TableExpression` that you extend through various function to add clauses.
634+- For our queries, we will share a common `FromClause`.
641+FromClause schema params from
644+- We need Schema, input parameters and a `FromType` giving the available fields of the expression.
646+- In our case, we'll join the table `mp` and the table `groupp`, meaning the `from` will be all fields of these tables.
648+- We can use `TableToRow` to be more DRY:
651+type BaseParliamentSelection =
652+ '[ "g" ::: TableToRow GroupCols
653+ , "m" ::: TableToRow MpCols ]
656+- Note that we've also put everything with table aliases: "g" and "m".
660+# Writing our from clause
663+baseParliamentTables ::
664+ FromClause Schema (param :: [NullityType]) BaseParliamentSelection
665+baseParliamentTables =
669+ (table (#mp `as` #m))
670+ (#m ! #mp_group .== #g ! #groupp_id)
673+- When picking "from" something, you need to specify the type with a function: `table` for a table, `view` for a view, etc.
675+- All joins are available. `innerJoin` is the most basic one. It takes the joined table and the joining condition.
677+- We have to specifiy the type of `param` (current limitation of the lib), even though _any_ params will be compatible with this, so we can plug `where` clauses depending on params should we need to !
681+# Typing the common selection
683+- Both our querie will need the same fields. This has to be factorized too.
685+- The scary signature of selection fields:
688+NP (Aliased (Expression schema from grouping params)) cols
691+- `NP` is for heterogenous lists ("n-ary product").
693+- `from` is the virtual table type, so our `BaseParliamentSelection`.
695+- `grouping` is there to make a distinction between aggregated / unaggregated queries.
697+- And finally, cols is the return type.
701+# Our common selection
705+ NP (Aliased (Expression Schema BaseParliamentSelection 'Ungrouped param))
708+ #g ! #name `as` #groupName
709+ :* #m ! #first_name `as` #firstName
710+ :* #m ! #last_name `as` #lastName
713+- I hope you like big signatures.
715+- But it's what let GHC checks that all alias and columns are available.
717+- And that you're returning what you really intend to return.
721+# Putting it all together
724+selectParliament :: Query Schema '[] GroupRowResult
726+ select groupSelection (from baseParliamentTables)
730+selectGroupMembers :: Query Schema '[ 'NotNull 'PGtext] GroupRowResult
732+ select groupSelection
733+ (from baseParliamentTables
734+ & where_ (#g ! #name .== param @1))
737+- That is fairly DRY. And _entirely typesafe_.
741+# Actually fetching the data
743+- We'll build an intermediary datatype representing our rows.
745+- We'll make it match our row.
747+- And we'll add generic-SOP so that we can build them from query results.
750+type GroupRowResult =
751+ '[ "groupName" ::: 'NotNull 'PGtext
752+ , "firstName" ::: 'NotNull 'PGtext
753+ , "lastName" ::: 'NotNull 'PGtext ]
758+ GroupRow { groupName :: Text
760+ , lastName :: Text }
763+instance SOP.Generic GroupRow
764+instance SOP.HasDatatypeInfo GroupRow
769+# Aggregate result logic
771+- We will get tabular, SQL data.
773+- Our results will be `[GroupRow]`.
775+- We'll build `Groups` from this:
778+buildGroup :: NE.NonEmpty GroupRow -> Group
780+ let buildMP (GroupRow _ f l) = Mp f l
781+ name = groupName . NE.head $ grs
782+ members = NE.toList (buildMP <$> grs)
785+rowToGroups :: [GroupRow] -> [Group]
787+ let grouped = fmap NE.fromList . L.groupBy ((==) `on` groupName)
788+ in fmap buildGroup . grouped
793+# Finally calling our queries
795+- Query with params will use `runQueryParams`.
797+- Query without params will use `runQuery`.
799+- Result is inside a `MonadPQ` and of type `K` from generic-sop.
801+- You get your actual result using `getRows`. Exemple:
804+getParliament :: (MonadPQ Schema m, MonadBaseControl IO m) => m [Group]
806+ res <- runQuery selectParliament
808+ pure $ rowToGroups rows
811+- In real life of course you'll write:
815+ rowToGroups <$> (runQuery selectParliament >>= getRows)
820+# Our query with params
824+ (MonadPQ Schema m, MonadBaseControl IO m) => Text -> m (Maybe Group)
826+ listToMaybe . rowToGroups <$>
827+ (runQueryParams selectGroupMembers (Only t) >>= getRows)
834+class: center, middle
837+
841+class: center, middle
850+- Not everything is included: some cool stuff like array_agg and window functions are not available yet.
852+- But development is _very_ active. `IN` clauses were missing but were added in the 4.0 released recently.
854+- It's bleeding edge. You need latest LTS to be comfortable and psql >= 9.5.
856+- It's Postgres-only but I would say that's a feature. Multi-DB tools are even more complex.
858+- The author & maintainer is <3.
860+- Typesafe. 'nuff said.
863+class: center, middle
870+ <script src="https://remarkjs.com/downloads/remark-latest.min.js">
873+ var slideshow = remark.create({countIncrementalSlides: false});