on master help sign in
Move getting start guide back to Read the Docs
commit04155d68b6dcfb79e4ab685ceeb69266c112550d authorOllie Charles <ollie@ocharles.org.uk> dateThu 1 Apr 2021, 21:17 (6 years ago) parent61ac6c7
7 files changed+465−374
docs/Makefile+19 docs/conf.py+32 docs/index.rst+37 docs/requirements.txt+3 docs/tutorial.rst+368 shell.nix+1−1 src/Rel8.hs+5−373
docs/Makefilenew file+19
@@ -0,0 +1,19 @@
1+# Minimal makefile for Sphinx documentation
2+#
3+
4+# You can set these variables from the command line.
5+SPHINXOPTS =
6+SPHINXBUILD = sphinx-build
7+SOURCEDIR = .
8+BUILDDIR = _build
9+
10+# Put it first so that "make" without argument is like "make help".
11+help:
12+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
13+
14+.PHONY: help Makefile
15+
16+# Catch-all target: route all unknown targets to Sphinx using the new
17+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
18+%: Makefile
19+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
docs/conf.pynew file+32
@@ -0,0 +1,32 @@
1+import sys
2+import os
3+import sphinx_rtd_theme
4+
5+# Support for :base-ref:, etc.
6+sys.path.insert(0, os.path.abspath('.'))
7+
8+version = "1.0.0"
9+
10+extensions = ['sphinx.ext.extlinks', 'sphinx.ext.todo']
11+
12+templates_path = ['_templates']
13+source_suffix = '.rst'
14+source_encoding = 'utf-8-sig'
15+master_doc = 'index'
16+
17+project = u'Rel8'
18+copyright = u'2021 CircuitHub'
19+release = version
20+
21+highlight_language = 'haskell'
22+primary_domain = 'haskell'
23+
24+exclude_patterns = ['.build', "*.gen.rst"]
25+
26+# on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org
27+on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
28+
29+if not on_rtd: # only import and set the theme if we're building docs locally
30+ import sphinx_rtd_theme
31+ html_theme = 'sphinx_rtd_theme'
32+ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
docs/index.rstnew file+37
@@ -0,0 +1,37 @@
1+Welcome to Rel8!
2+================================
3+
4+Welcome to Rel8! Rel8 is a Haskell library for interacting with PostgreSQL
5+databases, built on top of the fantastic `Opaleye
6+<https://hackage.haskell.org/package/opaleye>`_ library.
7+
8+The main objectives of Rel8 are:
9+
10+* *Conciseness*: Users using Rel8 should not need to write boiler-plate code.
11+ By using expressive types, we can provide sufficient information for the
12+ compiler to infer code whenever possible.
13+
14+* *Inferrable*: Despite using a lot of type level magic, Rel8 aims to have
15+ excellent and predictable type inference.
16+
17+* *Familiar*: writing Rel8 queries should feel like normal Haskell programming.
18+
19+.. toctree::
20+ :maxdepth: 2
21+ :caption: Contents:
22+
23+ tutorial
24+
25+
26+More Resources
27+==============
28+
29+* The `Haskell API documentation <https://hackage.haskell.org/package/rel8>`_
30+ describes how individual functions are types are to be used.
31+
32+* If you have a question about how to use Rel8, feel free to open a `GitHub
33+ discussion <https://github.com/circuithub/rel8/discussions>`_.
34+
35+* If you think you've found a bug, confusing behavior, or have a feature
36+ request, please raise an issue at `Rel8's issue tracker
37+ <https://github.com/circuithub/rel8>`_.
docs/requirements.txtnew file+3
@@ -0,0 +1,3 @@
1+sphinx == 3.1.*
2+sphinx_rtd_theme
3+
docs/tutorial.rstnew file+368
@@ -0,0 +1,368 @@
1+.. highlight:: haskell
2+
3+Getting Started
4+===============
5+
6+In this section, we'll take a look at using Rel8 to work with a small database
7+for Haskell packages. We'll take a look at idiomatic usage of Rel8, mapping
8+tables to Haskell, and then look at writing some simple queries.
9+
10+Before we get started, we'll be using the following language extensions and
11+imports throughout this guide::
12+
13+ {-# language -XBlockArguments #-}
14+ {-# language -XDeriveAnyClass #-}
15+ {-# language -XDeriveGeneric #-}
16+ {-# language -XDerivingStrategies #-}
17+ {-# language -XDerivingVia #-}
18+ {-# language -XDuplicateRecordFields #-}
19+ {-# language -XGeneralizedNewtypeDeriving #-}
20+ {-# language -XOverloadedStrings #-}
21+ {-# language -XStandaloneDeriving #-}
22+ {-# language -XTypeApplications #-}
23+ {-# language -XTypeFamilies #-}
24+
25+ import Prelude
26+
27+The Example Schema
28+------------------
29+
30+Before we start writing any Haskell, let's take a look at the schema we'll work
31+with. The `author` table has three columns:
32+
33++-----------------+-------------+----------+
34+| Column Name | Type | Nullable |
35++=================+=============+==========+
36+| ``author_id`` | ``integer`` | not null |
37++-----------------+-------------+----------+
38+| ``name`` | ``text`` | not null |
39++-----------------+-------------+----------+
40+| ``url`` | ``text`` | |
41++-----------------+-------------+----------+
42+
43+and the `project` table has two:
44+
45++-----------------+-------------+----------+
46+| Column Name | Type | Nullable |
47++=================+=============+==========+
48+| ``author_id`` | ``integer`` | not null |
49++-----------------+-------------+----------+
50+| ``name`` | ``text`` | not null |
51++-----------------+-------------+----------+
52+
53+A ``project`` always has an ``author``, but not all ``author``\s have projects.
54+Each ``author`` has a name and (maybe) an associated website, and each project
55+has a name.
56+
57+Mapping Schemas to Haskell
58+--------------------------
59+
60+Now that we've seen our schema, we can begin writing a mapping in Rel8. The
61+idiomatic way to map a table is to use a record that is parameterised what Rel8
62+calls an *interpretation functor*, and to define each field with ``Column``.
63+For this type to be usable with Rel8 we need it to be an instance of
64+``Rel8able``, which can be derived with a combination of ``DeriveAnyClass`` and
65+``DeriveGeneric`` language extensions.
66+
67+Following these steps for ``author``, we have::
68+
69+ data Author f = Author
70+ { authorId :: Column f Int64
71+ , name :: Column f Text
72+ , url :: Column f (Maybe Text)
73+ }
74+ deriving stock (Generic)
75+ deriving anyclass (Rel8able)
76+
77+This is a perfectly reasonable definition, but cautious readers might notice a
78+problem - in particular, with the type of the ``authorId`` field. While
79+``Int64`` is correct, it's not the best type. If we had other identifier types
80+in our project, it would be too easy to accidentally mix them up and create
81+nonsensical joins. As Haskell programmers, we often solve this problem by
82+creating ``newtype`` wrappers, and we can also use this technique with Rel8::
83+
84+ newtype AuthorId = AuthorId { toInt64 :: Int64 }
85+ deriving newtype (DBEq, DBType, Eq, Show)
86+
87+Now we can write our final schema mapping. First, the ``author`` table::
88+
89+ data Author f = Author
90+ { authorId :: Column f AuthorId
91+ , authorName :: Column f Text
92+ , authorUrl :: Column f (Maybe Text)
93+ }
94+ deriving stock (Generic)
95+ deriving anyclass (Rel8able)
96+
97+And similarly, the ``project`` table::
98+
99+ data Project f = Project
100+ { projectAuthorId :: Column f AuthorId
101+ , projectName :: Column f Text
102+ }
103+ deriving stock (Generic)
104+ deriving anyclass (Rel8able
105+
106+To show query results in this documentation, we'll also need ``Show`` instances:
107+Unfortunately these definitions look a bit scary, but they are essentially just
108+``deriving (Show)``::
109+
110+ deriving stock instance f ~ Identity => Show (Author f)
111+ deriving stock instance f ~ Identity => Show (Project f)
112+
113+These data types describe the structural mapping of the tables, but we also
114+need to specify a ``TableSchema`` for each table. A ``TableSchema`` contains
115+the name of the table and the name of all columns in the table, which will
116+ultimately allow us to ``SELECT`` and ``INSERT`` rows for these tables.
117+
118+To define a ``TableSchema``, we just need to fill construct appropriate
119+``TableSchema`` values. When it comes to the ``tableColumns`` field, we
120+construct values of our data types above, and set each field to the name of the
121+column that it maps to.
122+
123+First, ``authorSchema`` describes the column names of the ``author`` table when
124+associated with the ``Author`` type::
125+
126+ authorSchema :: TableSchema (Author Name)
127+ authorSchema = TableSchema
128+ { tableName = "author"
129+ , tableSchema = Nothing
130+ , tableColumns = Author
131+ { authorId = "author_id"
132+ , authorName = "name"
133+ , authorUrl = "url"
134+ }
135+ }
136+
137+And likewise for ``project`` and ``Project``::
138+
139+ projectSchema :: TableSchema (Project Name)
140+ projectSchema = TableSchema
141+ { tableName = "project"
142+ , tableSchema = Nothing
143+ , tableColumns = Project
144+ { projectAuthorId = "author_id"
145+ , projectName = "name"
146+ }
147+ }
148+
149+
150+.. note::
151+
152+ You might be wondering why this information isn't in the definitions of
153+ ``Author`` and ``Project`` above. Rel8 decouples ``TableSchema`` from the data
154+ types themselves, as not all tables you define will necessarily have a schema.
155+ For example, Rel8 allows you to define helper types to simplify the types of
156+ queries - these tables only exist at query time, but there is no corresponding
157+ base table. We'll see more on this idea later!
158+
159+With these table definitions, we can now start writing some queries!
160+
161+Writing Queries
162+---------------
163+
164+Simple Queries
165+~~~~~~~~~~~~~~
166+
167+First, we'll take a look at ``SELECT`` statements - usually the bulk of most
168+database heavy applications.
169+
170+In Rel8, ``SELECT`` statements are built using the ``Query`` monad. You can
171+think of this monad like the ordinary ``[]`` (List) monad - but this isn't
172+required knowledge.
173+
174+To start, we'll look at one of the simplest queries possible - a basic ``SELECT
175+* FROM`` statement. To select all rows from a table, we use ``each``, and
176+supply a ``TableSchema``. So to select all ``project`` rows, we can write::
177+
178+ >>> :t each projectSchema
179+ each projectSchema :: Query (Project Expr)
180+
181+Notice that ``each`` gives us a ``Query`` that yields ``Project Expr`` rows. To
182+see what this means, let's have a look at a single field of a ``Project Expr``::
183+
184+ >>> let aProjectExpr = undefined :: Project Expr
185+ >>> :t projectAuthorId aProjectExpr
186+ projectAuthorId aProjectExpr :: Expr AuthorId
187+
188+Recall we defined ``projectAuthorId`` as ``Column f AuthorId``. Now we have
189+``f`` is ``Expr``, and ``Column Expr AuthorId`` reduces to ``Expr AuthorId``.
190+We'll see more about ``Expr`` soon, but you can think of ``Expr a`` as "SQL
191+expressions of type ``a``\".
192+
193+To execute this ``Query``, we pass it to ``select``::
194+
195+ >>> :t select c (each projectSchema)
196+ select c (each projectSchema) :: MonadIO m => m [Project Identity]
197+
198+When we ``select`` things containing ``Expr``s, Rel8 builds a new response
199+table with the ``Identity`` interpretation. This means you'll get back plain
200+Haskell values. Studying ``projectAuthorId`` again, we have::
201+
202+ >>> let aProjectIdentity = undefined :: Project Identity
203+ >>> :t projectAuthorId aProjectIdentity
204+ projectAuthorId aProjectIdentity :: AuthorId
205+
206+Here ``Column Identity AuthorId`` reduces to just ``AuthorId``, with no
207+wrappping type at all.
208+
209+Putting this all together, we can run our first query::
210+
211+ >>> select c (each projectSchema) >>= mapM_ print
212+ Project {projectAuthorId = 1, projectName = "rel8"}
213+ Project {projectAuthorId = 2, projectName = "aeson"}
214+ Project {projectAuthorId = 2, projectName = "text"}
215+
216+We now know that ``each`` is the equivalent of a ``SELECT *`` query, but
217+sometimes we're only interested in a subset of the columns of a table. To
218+restrict the returned columns, we can specify a projection by using ``Query``\s
219+``Functor`` instance::
220+
221+ >>> select c $ projectName <$> each projectSchema
222+ ["rel8","aeson","text"]
223+
224+Joins
225+~~~~~
226+
227+Another common operation in relational databases is to take the ``JOIN`` of
228+multiple tables. Rel8 doesn't have a specific join operation, but we can
229+recover the functionality of a join by selecting all rows of two tables, and
230+then using ``where_`` to filter them.
231+
232+To see how this works, first let's look at taking the product of two tables.
233+We can do this by simply calling ``each`` twice, and then returning a tuple of
234+their results::
235+
236+ >>> :{
237+ mapM_ print =<< select c do
238+ author <- each authorSchema
239+ project <- each projectSchema
240+ return (projectName project, authorName author)
241+ :}
242+ ("rel8","Ollie")
243+ ("rel8","Bryan O'Sullivan")
244+ ("rel8","Emily Pillmore")
245+ ("aeson","Ollie")
246+ ("aeson","Bryan O'Sullivan")
247+ ("aeson","Emily Pillmore")
248+ ("text","Ollie")
249+ ("text","Bryan O'Sullivan")
250+ ("text","Emily Pillmore")
251+
252+This isn't quite right, though, as we have ended up pairing up the wrong
253+projects and authors. To fix this, we can use ``where_`` to restrict the
254+returned rows. We could write::
255+
256+ select c $ do
257+ author <- each authorSchema
258+ project <- each projectSchema
259+ where_ $ projectAuthorId project ==. authorId author
260+ return (project, author)
261+
262+but doing this every time you need a join can obscure the meaning of the
263+query you're writing. A good practice is to introduce specialised functions
264+for the particular joins in your database. In our case, this would be::
265+
266+ projectsForAuthor :: Author Expr -> Query (Project Expr)
267+ projectsForAuthor a = each projectSchema >>= filter \p ->
268+ projectAuthorId p ==. authorId a
269+
270+Our final query is then::
271+
272+ >>> :{
273+ mapM_ print =<< select c do
274+ author <- each authorSchema
275+ project <- projectsForAuthor author
276+ return (projectName project, authorName author)
277+ :}
278+ ("rel8","Ollie")
279+ ("aeson","Bryan O'Sullivan")
280+ ("text","Bryan O'Sullivan")
281+
282+Left Joins
283+~~~~~~~~~~
284+
285+Rel8 is also capable of performing ``LEFT JOIN``s. To perform ``LEFT JOIN``\s,
286+we follow the same approach as before, but use the ``optional`` query
287+transformer to allow for the possibility of the join to fail.
288+
289+In our test database, we can see that there's another author who we haven't
290+seen yet::
291+
292+ >>> select c $ authorName <$> each authorSchema
293+ ["Ollie","Bryan O'Sullivan","Emily Pillmore"]
294+
295+Emily wasn't returned in our earlier query because - in our database - she
296+doesn't have any registered projects. We can account for this partiality in our
297+original query by wrapping the ``projectsForAuthor`` call with ``optional``::
298+
299+ >>> :{
300+ mapM_ print =<< select c do
301+ author <- each authorSchema
302+ mproject <- optional $ projectsForAuthor author
303+ return (authorName author, projectName <$> mproject)
304+ :}
305+ ("Ollie",Just "rel8")
306+ ("Bryan O'Sullivan",Just "aeson")
307+ ("Bryan O'Sullivan",Just "text")
308+ ("Emily Pillmore",Nothing)
309+
310+
311+Aggregation
312+~~~~~~~~~~~
313+
314+Aggregations are operations like ``sum`` and ``count`` - operations that reduce
315+multiple rows to single values. To perform aggregations in Rel8, we can use the
316+``aggregate`` function, which takes a ``Query`` of aggregated expressions, runs
317+the aggregation, and returns aggregated rows.
318+
319+To start, let's look at a simple aggregation that tells us how many projects
320+exist::
321+
322+ >>> error "TODO"
323+
324+Rel8 is also capable of aggregating multiple rows into a single row by
325+concatenating all rows as a list. This aggregation allows us to break free of
326+the row-orientated nature of SQL and write queries that return tree-like
327+structures. Earlier we saw an example of returning authors with their projects,
328+but the query didn't do a great job of describing the one-to-many relationship
329+between authors and their projects.
330+
331+Let's look again at a query that returns authors and their projects, and
332+focus on the /type/ of that query::
333+
334+ projectsForAuthor a = each projectSchema >>= filter \p ->
335+ projectAuthorId p ==. authorId a
336+
337+ let authorsAndProjects = do
338+ author <- each authorSchema
339+ project <- projectsForAuthor author
340+ return (author, project)
341+ where
342+
343+ >>> :t select c authorsAndProjects
344+ select c authorsAndProjects
345+ :: MonadIO m => m [(Author Identity, Project Identity)]
346+
347+
348+Our query gives us a single list of pairs of authors and projects. However,
349+with our domain knowledge of the schema, this isn't a great type - what we'd
350+rather have is a list of pairs of authors and /lists/ of projects. That is,
351+what we'd like is::
352+
353+ [(Author Identity, [Project Identity])]
354+
355+This would be a much better type! Rel8 can produce a query with this type by
356+simply wrapping the call to ``projectsForAuthor`` with either ``some`` or
357+``many``. Here we'll use ``many``, which allows for the possibility of an
358+author to have no projects::
359+
360+ >>> :{
361+ mapM_ print =<< select c do
362+ author <- each authorSchema
363+ projectNames <- many $ projectName <$> projectsForAuthor author
364+ return (authorName author, projectNames)
365+ :}
366+ ("Ollie",["rel8"])
367+ ("Bryan O'Sullivan",["aeson","text"])
368+ ("Emily Pillmore",[])
shell.nix+1−1
@@ -7,5 +7,5 @@ in
77 withHoogle = true;
88 tools = { cabal = "3.2.0.0"; haskell-language-server = "latest"; };
99 exactDeps = false;
10− buildInputs = [ pkgs.postgresql ];
10+ buildInputs = [ pkgs.postgresql pkgs.pythonPackages.sphinx ];
1111 }
src/Rel8.hs+5−373
@@ -1,19 +1,7 @@
11{-# language DuplicateRecordFields #-}
22
33module Rel8
4− ( -- * Getting Started
5− -- $setup
6−
7− -- ** Writing Queries
8− -- $guideQueries
9−
10− -- ** Joins
11− -- $guideJoins
12−
13− -- ** Aggregation
14− -- $guideAggregation
15−
16− -- * Database types
4+ ( -- * Database types
175 -- ** @DBType@
186 DBType(..)
197
@@ -256,9 +244,11 @@ import Rel8.Expr.Bool
256244import Rel8.Expr.Eq
257245import Rel8.Expr.Function
258246import Rel8.Expr.Null
247+import Rel8.Expr.Opaleye (unsafeCastExpr)
259248import Rel8.Expr.Ord
260249import Rel8.Expr.Order
261250import Rel8.Expr.Serialize
251+import Rel8.Kind.Necessity
262252import Rel8.Order
263253import Rel8.Query
264254import Rel8.Query.Aggregate
@@ -278,6 +268,7 @@ import Rel8.Query.These
278268import Rel8.Query.Values
279269import Rel8.Schema.Column
280270import Rel8.Schema.Context.Label
271+import Rel8.Schema.Field (Field)
281272import Rel8.Schema.Generic
282273import Rel8.Schema.HTable
283274import Rel8.Schema.Name
@@ -307,8 +298,8 @@ import Rel8.Table.These
307298import Rel8.Type
308299import Rel8.Type.Eq
309300import Rel8.Type.Information
310−import Rel8.Type.JSONEncoded
311301import Rel8.Type.JSONBEncoded
302+import Rel8.Type.JSONEncoded
312303import Rel8.Type.Monoid
313304import Rel8.Type.Num
314305import Rel8.Type.Ord
@@ -316,364 +307,5 @@ import Rel8.Type.ReadShow
316307import Rel8.Type.Semigroup
317308import Rel8.Type.String
318309import Rel8.Type.Sum
319−import Rel8.Expr.Opaleye (unsafeCastExpr)
320−import Rel8.Kind.Necessity
321−import Rel8.Schema.Field (Field)
322310
323311
324−-- $setup
325−--
326−-- In this section, we'll take a look at using Rel8 to work with a small
327−-- database for Haskell packages. We'll take a look at idiomatic usage of Rel8,
328−-- mapping tables to Haskell, and then look at writing some simple queries.
329−--
330−-- The documentation in this library uses the following language extensions:
331−--
332−-- >>> :set -XBlockArguments
333−-- >>> :set -XDeriveAnyClass
334−-- >>> :set -XDeriveGeneric
335−-- >>> :set -XDerivingStrategies
336−-- >>> :set -XDerivingVia
337−-- >>> :set -XDuplicateRecordFields
338−-- >>> :set -XGeneralizedNewtypeDeriving
339−-- >>> :set -XOverloadedStrings
340−-- >>> :set -XStandaloneDeriving
341−-- >>> :set -XTypeApplications
342−-- >>> :set -XTypeFamilies
343−--
344−-- Before we start writing any Haskell, let's take a look at the schema we'll
345−-- work with.
346−--
347−-- > author: project:
348−-- >
349−-- > Column │ Type │ Nullable Column │ Type │ Nullable
350−-- > ═══════════╪═════════╪══════════ ═══════════╪═════════╪══════════
351−-- > author_id │ integer │ not null author_id │ integer │ not null
352−-- > name │ text │ not null name │ text │ not null
353−-- > url │ text │
354−--
355−-- Our schema consists of two tables - @author@ and @project@. A @project@
356−-- always has an @author@, but not all @author@s have projects. Each @author@
357−-- has a name and (maybe) an associated website, and each project has a name.
358−--
359−-- Now that we've seen our schema, we can begin writing a mapping in Rel8. The
360−-- idiomatic way to map a table is to use a record that is parameterised what
361−-- Rel8 calls an /interpretation functor/, and to define each field with
362−-- 'Column'. For this type to be usable with Rel8 we need it to be an instance
363−-- of 'HigherKindedTable', which can be derived with a combination of
364−-- @DeriveAnyClass@ and @DeriveGeneric@.
365−--
366−-- Following these steps for @author@, we have:
367−--
368−-- > data Author f = Author
369−-- > { authorId :: Column f Int64
370−-- > , name :: Column f Text
371−-- > , url :: Column f (Maybe Text)
372−-- > } deriving (Generic, HigherKindedTable)
373−--
374−-- This is a perfectly reasonable definition, but cautious readers might notice
375−-- a problem - in particular, with the type of the @authorId@ field. While
376−-- @Int64@ is correct, it's not the best type. If we had other identifier types
377−-- in our project, it would be too easy to accidentally mix them up and create
378−-- nonsensical joins. As Haskell programmers, we often solve this problem by
379−-- creating @newtype@ wrappers, and we can also use this technique with Rel8:
380−--
381−-- >>> :{
382−-- newtype AuthorId = AuthorId { toInt64 :: Int64 }
383−-- deriving newtype (DBEq, DBType, Eq, Show)
384−-- :}
385−--
386−-- Now we can write our final schema mapping. First, the @author@ table:
387−--
388−-- >>> :{
389−-- data Author f = Author
390−-- { authorId :: Column f AuthorId
391−-- , authorName :: Column f Text
392−-- , authorUrl :: Column f (Maybe Text)
393−-- }
394−-- deriving stock Generic
395−-- deriving anyclass HigherKindedTable
396−-- :}
397−--
398−-- And similarly, the @project@ table:
399−--
400−-- >>> :{
401−-- data Project f = Project
402−-- { projectAuthorId :: Column f AuthorId
403−-- , projectName :: Column f Text
404−-- }
405−-- deriving stock Generic
406−-- deriving anyclass HigherKindedTable
407−-- :}
408−--
409−-- To show query results in this documentation, we'll also need @Show@
410−-- instances: Unfortunately these definitions look a bit scary, but they are
411−-- essentially just @deriving (Show)@:
412−--
413−-- >>> deriving stock instance f ~ Identity => Show (Author f)
414−-- >>> deriving stock instance f ~ Identity => Show (Project f)
415−--
416−-- These data types describe the structural mapping of the tables, but we also
417−-- need to specify a 'TableSchema' for each table. A @TableSchema@ contains the
418−-- name of the table and the name of all columns in the table, which will
419−-- ultimately allow us to @SELECT@ and @INSERT@ rows for these tables.
420−--
421−-- To define a @TableSchema@, we just need to fill construct appropriate
422−-- @TableSchema@ values. When it comes to the @tableColumns@ field, we
423−-- construct values of our data types above, and set each field to the name of
424−-- the column that it maps to:
425−--
426−-- First, @authorSchema@ describes the column names of the @author@ table when
427−-- associated with the @Author@ type:
428−--
429−-- >>> :{
430−-- authorSchema :: TableSchema (Author ColumnSchema)
431−-- authorSchema = TableSchema
432−-- { tableName = "author"
433−-- , tableSchema = Nothing
434−-- , tableColumns = Author
435−-- { authorId = "author_id"
436−-- , authorName = "name"
437−-- , authorUrl = "url"
438−-- }
439−-- }
440−-- :}
441−--
442−-- And likewise for @project@ and @Project@:
443−--
444−-- >>> :{
445−-- projectSchema :: TableSchema (Project ColumnSchema)
446−-- projectSchema = TableSchema
447−-- { tableName = "project"
448−-- , tableSchema = Nothing
449−-- , tableColumns = Project
450−-- { projectAuthorId = "author_id"
451−-- , projectName = "name"
452−-- }
453−-- }
454−-- :}
455−--
456−-- Aside: you might be wondering why this information isn't in the definitions
457−-- of @Author@ and @Project@ above. Rel8 decouples @TableSchema@ from the data
458−-- types themselves, as not all tables you define will necessarily have a
459−-- schema. For example, Rel8 allows you to define helper types to simplify the
460−-- types of queries - these tables only exist at query time, but there is no
461−-- corresponding base table. We'll see more on this idea later!
462−--
463−-- With these table definitions, we can now start writing some queries! To
464−-- actually run a query, we'll need a database connection. Rel8 uses the
465−-- @hasql@ library, and for this documentation we'll get the connection string
466−-- from the @$TEST_DATABASE_URL@ environment variable.
467−--
468−-- >>> connectionString <- System.Environment.getEnv "TEST_DATABASE_URL"
469−-- >>> Right c <- Hasql.Connection.acquire (Data.ByteString.Char8.pack connectionString)
470−-- >>> Control.Monad.void $ Hasql.Session.run (Hasql.Session.sql "BEGIN") c
471−
472−-- $guideQueries
473−--
474−-- First, we'll take a look at @SELECT@ statements - usually the bulk of most
475−-- database heavy applications.
476−--
477−-- In Rel8, @SELECT@ statements are built using the 'Query' monad. You can
478−-- think of this monad like the ordinary @[]@ (List) monad - but this isn't
479−-- required knowledge.
480−--
481−-- To start, we'll look at one of the simplest queries possible - a basic
482−-- @SELECT * FROM@ statement. To select all rows from a table, we use 'each',
483−-- and supply a 'TableSchema'. So to select all @project@ rows, we can write:
484−--
485−-- >>> :t each projectSchema
486−-- each projectSchema :: Query (Project Expr)
487−--
488−-- Notice that @each@ gives us a @Query@ that yields @Project Expr@ rows. To
489−-- see what this means, let's have a look at a single field of a
490−-- @Project Expr@:
491−--
492−-- >>> let aProjectExpr = undefined :: Project Expr
493−-- >>> :t projectAuthorId aProjectExpr
494−-- projectAuthorId aProjectExpr :: Expr AuthorId
495−--
496−-- Recall we defined @projectAuthorId@ as @Column f AuthorId@. Now we have @f@
497−-- is @Expr@, and @Column Expr AuthorId@ reduces to @Expr AuthorId@. We'll see
498−-- more about @Expr@ soon, but you can think of @Expr a@ as "SQL expressions of
499−-- type @a@".
500−--
501−-- To execute this @Query@, we pass it to 'select':
502−--
503−-- >>> :t select c (each projectSchema)
504−-- select c (each projectSchema) :: MonadIO m => m [Project Identity]
505−--
506−-- When we @select@ things containing @Expr@s, Rel8 builds a new response table
507−-- with the @Identity@ interpretation. This means you'll get back plain Haskell
508−-- values. Studying @projectAuthorId@ again, we have:
509−--
510−-- >>> let aProjectIdentity = undefined :: Project Identity
511−-- >>> :t projectAuthorId aProjectIdentity
512−-- projectAuthorId aProjectIdentity :: AuthorId
513−--
514−-- Here @Column Identity AuthorId@ reduces to just @AuthorId@, with no
515−-- wrappping type at all.
516−--
517−-- Putting this all together, we can run our first query:
518−--
519−-- >>> select c (each projectSchema) >>= mapM_ print
520−-- Project {projectAuthorId = 1, projectName = "rel8"}
521−-- Project {projectAuthorId = 2, projectName = "aeson"}
522−-- Project {projectAuthorId = 2, projectName = "text"}
523−--
524−-- Cool!
525−--
526−-- We now know that 'each' is the equivalent of a @SELECT *@ query, but
527−-- sometimes we're only interested in a subset of the columns of a table. To
528−-- restrict the returned columns, we can specify a projection by using 'Query's
529−-- @Functor@ instance:
530−--
531−-- >>> select c $ projectName <$> each projectSchema
532−-- ["rel8","aeson","text"]
533−
534−-- $guideJoins
535−--
536−-- Another common operation in relational databases is to take the @JOIN@ of
537−-- multiple tables. Rel8 doesn't have a specific join operation, but we can
538−-- recover the functionality of a join by selecting all rows of two tables, and
539−-- then using 'where_' to filter them.
540−--
541−-- To see how this works, first let's look at taking the product of two tables.
542−-- We can do this by simply calling 'each' twice, and then returning a tuple of
543−-- their results.
544−--
545−-- >>> :{
546−-- mapM_ print =<< select c do
547−-- author <- each authorSchema
548−-- project <- each projectSchema
549−-- return (projectName project, authorName author)
550−-- :}
551−-- ("rel8","Ollie")
552−-- ("rel8","Bryan O'Sullivan")
553−-- ("rel8","Emily Pillmore")
554−-- ("aeson","Ollie")
555−-- ("aeson","Bryan O'Sullivan")
556−-- ("aeson","Emily Pillmore")
557−-- ("text","Ollie")
558−-- ("text","Bryan O'Sullivan")
559−-- ("text","Emily Pillmore")
560−--
561−-- This isn't quite right, though, as we have ended up pairing up the wrong
562−-- projects and authors. To fix this, we can use 'where_' to restrict the
563−-- returned rows. We could write:
564−--
565−-- > select c $ do
566−-- > author <- each authorSchema
567−-- > project <- each projectSchema
568−-- > where_ $ projectAuthorId project ==. authorId author
569−-- > return (project, author)
570−--
571−-- but doing this every time you need a join can obscure the meaning of the
572−-- query you're writing. A good practice is to introduce specialised functions
573−-- for the particular joins in your database. In our case, this would be:
574−--
575−-- >>> :{
576−-- projectsForAuthor :: Author Expr -> Query (Project Expr)
577−-- projectsForAuthor a = each projectSchema >>= filter \p ->
578−-- projectAuthorId p ==. authorId a
579−-- :}
580−--
581−-- Our final query is then:
582−--
583−-- >>> :{
584−-- mapM_ print =<< select c do
585−-- author <- each authorSchema
586−-- project <- projectsForAuthor author
587−-- return (projectName project, authorName author)
588−-- :}
589−-- ("rel8","Ollie")
590−-- ("aeson","Bryan O'Sullivan")
591−-- ("text","Bryan O'Sullivan")
592−--
593−-- == @LEFT JOIN@s
594−--
595−-- Rel8 is also capable of performing @LEFT JOIN@s. To perform @LEFT JOIN@s, we
596−-- follow the same approach as before, but use the 'optional' query transformer
597−-- to allow for the possibility of the join to fail.
598−--
599−-- In our test database, we can see that there's another author who we haven't
600−-- seen yet:
601−--
602−-- >>> select c $ authorName <$> each authorSchema
603−-- ["Ollie","Bryan O'Sullivan","Emily Pillmore"]
604−--
605−-- Emily wasn't returned in our earlier query because - in our database - she
606−-- doesn't have any registered projects. We can account for this partiality in
607−-- our original query by wrapping the @projectsForAuthor@ call with 'optional':
608−--
609−-- >>> :{
610−-- mapM_ print =<< select c do
611−-- author <- each authorSchema
612−-- mproject <- optional $ projectsForAuthor author
613−-- return (authorName author, projectName <$> mproject)
614−-- :}
615−-- ("Ollie",Just "rel8")
616−-- ("Bryan O'Sullivan",Just "aeson")
617−-- ("Bryan O'Sullivan",Just "text")
618−-- ("Emily Pillmore",Nothing)
619−
620−-- $guideAggregation
621−--
622−-- Aggregations are operations like @sum@ and @count@ - operations that reduce
623−-- multiple rows to single values. To perform aggregations in Rel8, we can use
624−-- the 'aggregate' function, which takes a 'Query' of aggregated expressions,
625−-- runs the aggregation, and returns aggregated rows.
626−--
627−-- To start, let's look at a simple aggregation that tells us how many projects
628−-- exist:
629−--
630−-- >>> error "TODO"
631−--
632−-- Rel8 is also capable of aggregating multiple rows into a single row by
633−-- concatenating all rows as a list. This aggregation allows us to break free
634−-- of the row-orientated nature of SQL and write queries that return tree-like
635−-- structures. Earlier we saw an example of returning authors with their
636−-- projects, but the query didn't do a great job of describing the one-to-many
637−-- relationship between authors and their projects.
638−--
639−-- Let's look again at a query that returns authors and their projects, and
640−-- focus on the /type/ of that query.
641−--
642−-- >>> :{
643−-- projectsForAuthor a = each projectSchema >>= filter \p ->
644−-- projectAuthorId p ==. authorId a
645−-- :}
646−--
647−-- >>> :{
648−-- let authorsAndProjects = do
649−-- author <- each authorSchema
650−-- project <- projectsForAuthor author
651−-- return (author, project)
652−-- where
653−-- :}
654−--
655−-- >>> :t select c authorsAndProjects
656−-- select c authorsAndProjects
657−-- :: MonadIO m => m [(Author Identity, Project Identity)]
658−--
659−-- Our query gives us a single list of pairs of authors and projects. However,
660−-- with our domain knowledge of the schema, this isn't a great type - what we'd
661−-- rather have is a list of pairs of authors and /lists/ of projects. That is,
662−-- what we'd like is:
663−--
664−-- > [(Author Identity, [Project Identity])]
665−--
666−-- This would be a much better type! Rel8 can produce a query with this type by
667−-- simply wrapping the call to @projectsForAuthor@ with either 'some' or
668−-- 'many'. Here we'll use 'many', which allows for the possibility of an author
669−-- to have no projects:
670−--
671−-- >>> :{
672−-- mapM_ print =<< select c do
673−-- author <- each authorSchema
674−-- projectNames <- many $ projectName <$> projectsForAuthor author
675−-- return (authorName author, projectNames)
676−-- :}
677−-- ("Ollie",["rel8"])
678−-- ("Bryan O'Sullivan",["aeson","text"])
679−-- ("Emily Pillmore",[])