Move getting start guide back to Read the Docs

This commit is contained in:
Ollie Charles 2021-04-01 21:17:50 +01:00
parent 61ac6c73bf
commit 04155d68b6
7 changed files with 465 additions and 374 deletions

19
docs/Makefile Normal file
View File

@ -0,0 +1,19 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

32
docs/conf.py Normal file
View File

@ -0,0 +1,32 @@
import sys
import os
import sphinx_rtd_theme
# Support for :base-ref:, etc.
sys.path.insert(0, os.path.abspath('.'))
version = "1.0.0"
extensions = ['sphinx.ext.extlinks', 'sphinx.ext.todo']
templates_path = ['_templates']
source_suffix = '.rst'
source_encoding = 'utf-8-sig'
master_doc = 'index'
project = u'Rel8'
copyright = u'2021 CircuitHub'
release = version
highlight_language = 'haskell'
primary_domain = 'haskell'
exclude_patterns = ['.build', "*.gen.rst"]
# on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if not on_rtd: # only import and set the theme if we're building docs locally
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]

37
docs/index.rst Normal file
View File

@ -0,0 +1,37 @@
Welcome to Rel8!
================================
Welcome to Rel8! Rel8 is a Haskell library for interacting with PostgreSQL
databases, built on top of the fantastic `Opaleye
<https://hackage.haskell.org/package/opaleye>`_ library.
The main objectives of Rel8 are:
* *Conciseness*: Users using Rel8 should not need to write boiler-plate code.
By using expressive types, we can provide sufficient information for the
compiler to infer code whenever possible.
* *Inferrable*: Despite using a lot of type level magic, Rel8 aims to have
excellent and predictable type inference.
* *Familiar*: writing Rel8 queries should feel like normal Haskell programming.
.. toctree::
:maxdepth: 2
:caption: Contents:
tutorial
More Resources
==============
* The `Haskell API documentation <https://hackage.haskell.org/package/rel8>`_
describes how individual functions are types are to be used.
* If you have a question about how to use Rel8, feel free to open a `GitHub
discussion <https://github.com/circuithub/rel8/discussions>`_.
* If you think you've found a bug, confusing behavior, or have a feature
request, please raise an issue at `Rel8's issue tracker
<https://github.com/circuithub/rel8>`_.

3
docs/requirements.txt Normal file
View File

@ -0,0 +1,3 @@
sphinx == 3.1.*
sphinx_rtd_theme

368
docs/tutorial.rst Normal file
View File

@ -0,0 +1,368 @@
.. highlight:: haskell
Getting Started
===============
In this section, we'll take a look at using Rel8 to work with a small database
for Haskell packages. We'll take a look at idiomatic usage of Rel8, mapping
tables to Haskell, and then look at writing some simple queries.
Before we get started, we'll be using the following language extensions and
imports throughout this guide::
{-# language -XBlockArguments #-}
{-# language -XDeriveAnyClass #-}
{-# language -XDeriveGeneric #-}
{-# language -XDerivingStrategies #-}
{-# language -XDerivingVia #-}
{-# language -XDuplicateRecordFields #-}
{-# language -XGeneralizedNewtypeDeriving #-}
{-# language -XOverloadedStrings #-}
{-# language -XStandaloneDeriving #-}
{-# language -XTypeApplications #-}
{-# language -XTypeFamilies #-}
import Prelude
The Example Schema
------------------
Before we start writing any Haskell, let's take a look at the schema we'll work
with. The `author` table has three columns:
+-----------------+-------------+----------+
| Column Name | Type | Nullable |
+=================+=============+==========+
| ``author_id`` | ``integer`` | not null |
+-----------------+-------------+----------+
| ``name`` | ``text`` | not null |
+-----------------+-------------+----------+
| ``url`` | ``text`` | |
+-----------------+-------------+----------+
and the `project` table has two:
+-----------------+-------------+----------+
| Column Name | Type | Nullable |
+=================+=============+==========+
| ``author_id`` | ``integer`` | not null |
+-----------------+-------------+----------+
| ``name`` | ``text`` | not null |
+-----------------+-------------+----------+
A ``project`` always has an ``author``, but not all ``author``\s have projects.
Each ``author`` has a name and (maybe) an associated website, and each project
has a name.
Mapping Schemas to Haskell
--------------------------
Now that we've seen our schema, we can begin writing a mapping in Rel8. The
idiomatic way to map a table is to use a record that is parameterised what Rel8
calls an *interpretation functor*, and to define each field with ``Column``.
For this type to be usable with Rel8 we need it to be an instance of
``Rel8able``, which can be derived with a combination of ``DeriveAnyClass`` and
``DeriveGeneric`` language extensions.
Following these steps for ``author``, we have::
data Author f = Author
{ authorId :: Column f Int64
, name :: Column f Text
, url :: Column f (Maybe Text)
}
deriving stock (Generic)
deriving anyclass (Rel8able)
This is a perfectly reasonable definition, but cautious readers might notice a
problem - in particular, with the type of the ``authorId`` field. While
``Int64`` is correct, it's not the best type. If we had other identifier types
in our project, it would be too easy to accidentally mix them up and create
nonsensical joins. As Haskell programmers, we often solve this problem by
creating ``newtype`` wrappers, and we can also use this technique with Rel8::
newtype AuthorId = AuthorId { toInt64 :: Int64 }
deriving newtype (DBEq, DBType, Eq, Show)
Now we can write our final schema mapping. First, the ``author`` table::
data Author f = Author
{ authorId :: Column f AuthorId
, authorName :: Column f Text
, authorUrl :: Column f (Maybe Text)
}
deriving stock (Generic)
deriving anyclass (Rel8able)
And similarly, the ``project`` table::
data Project f = Project
{ projectAuthorId :: Column f AuthorId
, projectName :: Column f Text
}
deriving stock (Generic)
deriving anyclass (Rel8able
To show query results in this documentation, we'll also need ``Show`` instances:
Unfortunately these definitions look a bit scary, but they are essentially just
``deriving (Show)``::
deriving stock instance f ~ Identity => Show (Author f)
deriving stock instance f ~ Identity => Show (Project f)
These data types describe the structural mapping of the tables, but we also
need to specify a ``TableSchema`` for each table. A ``TableSchema`` contains
the name of the table and the name of all columns in the table, which will
ultimately allow us to ``SELECT`` and ``INSERT`` rows for these tables.
To define a ``TableSchema``, we just need to fill construct appropriate
``TableSchema`` values. When it comes to the ``tableColumns`` field, we
construct values of our data types above, and set each field to the name of the
column that it maps to.
First, ``authorSchema`` describes the column names of the ``author`` table when
associated with the ``Author`` type::
authorSchema :: TableSchema (Author Name)
authorSchema = TableSchema
{ tableName = "author"
, tableSchema = Nothing
, tableColumns = Author
{ authorId = "author_id"
, authorName = "name"
, authorUrl = "url"
}
}
And likewise for ``project`` and ``Project``::
projectSchema :: TableSchema (Project Name)
projectSchema = TableSchema
{ tableName = "project"
, tableSchema = Nothing
, tableColumns = Project
{ projectAuthorId = "author_id"
, projectName = "name"
}
}
.. note::
You might be wondering why this information isn't in the definitions of
``Author`` and ``Project`` above. Rel8 decouples ``TableSchema`` from the data
types themselves, as not all tables you define will necessarily have a schema.
For example, Rel8 allows you to define helper types to simplify the types of
queries - these tables only exist at query time, but there is no corresponding
base table. We'll see more on this idea later!
With these table definitions, we can now start writing some queries!
Writing Queries
---------------
Simple Queries
~~~~~~~~~~~~~~
First, we'll take a look at ``SELECT`` statements - usually the bulk of most
database heavy applications.
In Rel8, ``SELECT`` statements are built using the ``Query`` monad. You can
think of this monad like the ordinary ``[]`` (List) monad - but this isn't
required knowledge.
To start, we'll look at one of the simplest queries possible - a basic ``SELECT
* FROM`` statement. To select all rows from a table, we use ``each``, and
supply a ``TableSchema``. So to select all ``project`` rows, we can write::
>>> :t each projectSchema
each projectSchema :: Query (Project Expr)
Notice that ``each`` gives us a ``Query`` that yields ``Project Expr`` rows. To
see what this means, let's have a look at a single field of a ``Project Expr``::
>>> let aProjectExpr = undefined :: Project Expr
>>> :t projectAuthorId aProjectExpr
projectAuthorId aProjectExpr :: Expr AuthorId
Recall we defined ``projectAuthorId`` as ``Column f AuthorId``. Now we have
``f`` is ``Expr``, and ``Column Expr AuthorId`` reduces to ``Expr AuthorId``.
We'll see more about ``Expr`` soon, but you can think of ``Expr a`` as "SQL
expressions of type ``a``\".
To execute this ``Query``, we pass it to ``select``::
>>> :t select c (each projectSchema)
select c (each projectSchema) :: MonadIO m => m [Project Identity]
When we ``select`` things containing ``Expr``s, Rel8 builds a new response
table with the ``Identity`` interpretation. This means you'll get back plain
Haskell values. Studying ``projectAuthorId`` again, we have::
>>> let aProjectIdentity = undefined :: Project Identity
>>> :t projectAuthorId aProjectIdentity
projectAuthorId aProjectIdentity :: AuthorId
Here ``Column Identity AuthorId`` reduces to just ``AuthorId``, with no
wrappping type at all.
Putting this all together, we can run our first query::
>>> select c (each projectSchema) >>= mapM_ print
Project {projectAuthorId = 1, projectName = "rel8"}
Project {projectAuthorId = 2, projectName = "aeson"}
Project {projectAuthorId = 2, projectName = "text"}
We now know that ``each`` is the equivalent of a ``SELECT *`` query, but
sometimes we're only interested in a subset of the columns of a table. To
restrict the returned columns, we can specify a projection by using ``Query``\s
``Functor`` instance::
>>> select c $ projectName <$> each projectSchema
["rel8","aeson","text"]
Joins
~~~~~
Another common operation in relational databases is to take the ``JOIN`` of
multiple tables. Rel8 doesn't have a specific join operation, but we can
recover the functionality of a join by selecting all rows of two tables, and
then using ``where_`` to filter them.
To see how this works, first let's look at taking the product of two tables.
We can do this by simply calling ``each`` twice, and then returning a tuple of
their results::
>>> :{
mapM_ print =<< select c do
author <- each authorSchema
project <- each projectSchema
return (projectName project, authorName author)
:}
("rel8","Ollie")
("rel8","Bryan O'Sullivan")
("rel8","Emily Pillmore")
("aeson","Ollie")
("aeson","Bryan O'Sullivan")
("aeson","Emily Pillmore")
("text","Ollie")
("text","Bryan O'Sullivan")
("text","Emily Pillmore")
This isn't quite right, though, as we have ended up pairing up the wrong
projects and authors. To fix this, we can use ``where_`` to restrict the
returned rows. We could write::
select c $ do
author <- each authorSchema
project <- each projectSchema
where_ $ projectAuthorId project ==. authorId author
return (project, author)
but doing this every time you need a join can obscure the meaning of the
query you're writing. A good practice is to introduce specialised functions
for the particular joins in your database. In our case, this would be::
projectsForAuthor :: Author Expr -> Query (Project Expr)
projectsForAuthor a = each projectSchema >>= filter \p ->
projectAuthorId p ==. authorId a
Our final query is then::
>>> :{
mapM_ print =<< select c do
author <- each authorSchema
project <- projectsForAuthor author
return (projectName project, authorName author)
:}
("rel8","Ollie")
("aeson","Bryan O'Sullivan")
("text","Bryan O'Sullivan")
Left Joins
~~~~~~~~~~
Rel8 is also capable of performing ``LEFT JOIN``s. To perform ``LEFT JOIN``\s,
we follow the same approach as before, but use the ``optional`` query
transformer to allow for the possibility of the join to fail.
In our test database, we can see that there's another author who we haven't
seen yet::
>>> select c $ authorName <$> each authorSchema
["Ollie","Bryan O'Sullivan","Emily Pillmore"]
Emily wasn't returned in our earlier query because - in our database - she
doesn't have any registered projects. We can account for this partiality in our
original query by wrapping the ``projectsForAuthor`` call with ``optional``::
>>> :{
mapM_ print =<< select c do
author <- each authorSchema
mproject <- optional $ projectsForAuthor author
return (authorName author, projectName <$> mproject)
:}
("Ollie",Just "rel8")
("Bryan O'Sullivan",Just "aeson")
("Bryan O'Sullivan",Just "text")
("Emily Pillmore",Nothing)
Aggregation
~~~~~~~~~~~
Aggregations are operations like ``sum`` and ``count`` - operations that reduce
multiple rows to single values. To perform aggregations in Rel8, we can use the
``aggregate`` function, which takes a ``Query`` of aggregated expressions, runs
the aggregation, and returns aggregated rows.
To start, let's look at a simple aggregation that tells us how many projects
exist::
>>> error "TODO"
Rel8 is also capable of aggregating multiple rows into a single row by
concatenating all rows as a list. This aggregation allows us to break free of
the row-orientated nature of SQL and write queries that return tree-like
structures. Earlier we saw an example of returning authors with their projects,
but the query didn't do a great job of describing the one-to-many relationship
between authors and their projects.
Let's look again at a query that returns authors and their projects, and
focus on the /type/ of that query::
projectsForAuthor a = each projectSchema >>= filter \p ->
projectAuthorId p ==. authorId a
let authorsAndProjects = do
author <- each authorSchema
project <- projectsForAuthor author
return (author, project)
where
>>> :t select c authorsAndProjects
select c authorsAndProjects
:: MonadIO m => m [(Author Identity, Project Identity)]
Our query gives us a single list of pairs of authors and projects. However,
with our domain knowledge of the schema, this isn't a great type - what we'd
rather have is a list of pairs of authors and /lists/ of projects. That is,
what we'd like is::
[(Author Identity, [Project Identity])]
This would be a much better type! Rel8 can produce a query with this type by
simply wrapping the call to ``projectsForAuthor`` with either ``some`` or
``many``. Here we'll use ``many``, which allows for the possibility of an
author to have no projects::
>>> :{
mapM_ print =<< select c do
author <- each authorSchema
projectNames <- many $ projectName <$> projectsForAuthor author
return (authorName author, projectNames)
:}
("Ollie",["rel8"])
("Bryan O'Sullivan",["aeson","text"])
("Emily Pillmore",[])

View File

@ -7,5 +7,5 @@ in
withHoogle = true;
tools = { cabal = "3.2.0.0"; haskell-language-server = "latest"; };
exactDeps = false;
buildInputs = [ pkgs.postgresql ];
buildInputs = [ pkgs.postgresql pkgs.pythonPackages.sphinx ];
}

View File

@ -1,19 +1,7 @@
{-# language DuplicateRecordFields #-}
module Rel8
( -- * Getting Started
-- $setup
-- ** Writing Queries
-- $guideQueries
-- ** Joins
-- $guideJoins
-- ** Aggregation
-- $guideAggregation
-- * Database types
( -- * Database types
-- ** @DBType@
DBType(..)
@ -256,9 +244,11 @@ import Rel8.Expr.Bool
import Rel8.Expr.Eq
import Rel8.Expr.Function
import Rel8.Expr.Null
import Rel8.Expr.Opaleye (unsafeCastExpr)
import Rel8.Expr.Ord
import Rel8.Expr.Order
import Rel8.Expr.Serialize
import Rel8.Kind.Necessity
import Rel8.Order
import Rel8.Query
import Rel8.Query.Aggregate
@ -278,6 +268,7 @@ import Rel8.Query.These
import Rel8.Query.Values
import Rel8.Schema.Column
import Rel8.Schema.Context.Label
import Rel8.Schema.Field (Field)
import Rel8.Schema.Generic
import Rel8.Schema.HTable
import Rel8.Schema.Name
@ -307,8 +298,8 @@ import Rel8.Table.These
import Rel8.Type
import Rel8.Type.Eq
import Rel8.Type.Information
import Rel8.Type.JSONEncoded
import Rel8.Type.JSONBEncoded
import Rel8.Type.JSONEncoded
import Rel8.Type.Monoid
import Rel8.Type.Num
import Rel8.Type.Ord
@ -316,364 +307,5 @@ import Rel8.Type.ReadShow
import Rel8.Type.Semigroup
import Rel8.Type.String
import Rel8.Type.Sum
import Rel8.Expr.Opaleye (unsafeCastExpr)
import Rel8.Kind.Necessity
import Rel8.Schema.Field (Field)
-- $setup
--
-- In this section, we'll take a look at using Rel8 to work with a small
-- database for Haskell packages. We'll take a look at idiomatic usage of Rel8,
-- mapping tables to Haskell, and then look at writing some simple queries.
--
-- The documentation in this library uses the following language extensions:
--
-- >>> :set -XBlockArguments
-- >>> :set -XDeriveAnyClass
-- >>> :set -XDeriveGeneric
-- >>> :set -XDerivingStrategies
-- >>> :set -XDerivingVia
-- >>> :set -XDuplicateRecordFields
-- >>> :set -XGeneralizedNewtypeDeriving
-- >>> :set -XOverloadedStrings
-- >>> :set -XStandaloneDeriving
-- >>> :set -XTypeApplications
-- >>> :set -XTypeFamilies
--
-- Before we start writing any Haskell, let's take a look at the schema we'll
-- work with.
--
-- > author: project:
-- >
-- > Column │ Type │ Nullable Column │ Type │ Nullable
-- > ═══════════╪═════════╪══════════ ═══════════╪═════════╪══════════
-- > author_id │ integer │ not null author_id │ integer │ not null
-- > name │ text │ not null name │ text │ not null
-- > url │ text │
--
-- Our schema consists of two tables - @author@ and @project@. A @project@
-- always has an @author@, but not all @author@s have projects. Each @author@
-- has a name and (maybe) an associated website, and each project has a name.
--
-- Now that we've seen our schema, we can begin writing a mapping in Rel8. The
-- idiomatic way to map a table is to use a record that is parameterised what
-- Rel8 calls an /interpretation functor/, and to define each field with
-- 'Column'. For this type to be usable with Rel8 we need it to be an instance
-- of 'HigherKindedTable', which can be derived with a combination of
-- @DeriveAnyClass@ and @DeriveGeneric@.
--
-- Following these steps for @author@, we have:
--
-- > data Author f = Author
-- > { authorId :: Column f Int64
-- > , name :: Column f Text
-- > , url :: Column f (Maybe Text)
-- > } deriving (Generic, HigherKindedTable)
--
-- This is a perfectly reasonable definition, but cautious readers might notice
-- a problem - in particular, with the type of the @authorId@ field. While
-- @Int64@ is correct, it's not the best type. If we had other identifier types
-- in our project, it would be too easy to accidentally mix them up and create
-- nonsensical joins. As Haskell programmers, we often solve this problem by
-- creating @newtype@ wrappers, and we can also use this technique with Rel8:
--
-- >>> :{
-- newtype AuthorId = AuthorId { toInt64 :: Int64 }
-- deriving newtype (DBEq, DBType, Eq, Show)
-- :}
--
-- Now we can write our final schema mapping. First, the @author@ table:
--
-- >>> :{
-- data Author f = Author
-- { authorId :: Column f AuthorId
-- , authorName :: Column f Text
-- , authorUrl :: Column f (Maybe Text)
-- }
-- deriving stock Generic
-- deriving anyclass HigherKindedTable
-- :}
--
-- And similarly, the @project@ table:
--
-- >>> :{
-- data Project f = Project
-- { projectAuthorId :: Column f AuthorId
-- , projectName :: Column f Text
-- }
-- deriving stock Generic
-- deriving anyclass HigherKindedTable
-- :}
--
-- To show query results in this documentation, we'll also need @Show@
-- instances: Unfortunately these definitions look a bit scary, but they are
-- essentially just @deriving (Show)@:
--
-- >>> deriving stock instance f ~ Identity => Show (Author f)
-- >>> deriving stock instance f ~ Identity => Show (Project f)
--
-- These data types describe the structural mapping of the tables, but we also
-- need to specify a 'TableSchema' for each table. A @TableSchema@ contains the
-- name of the table and the name of all columns in the table, which will
-- ultimately allow us to @SELECT@ and @INSERT@ rows for these tables.
--
-- To define a @TableSchema@, we just need to fill construct appropriate
-- @TableSchema@ values. When it comes to the @tableColumns@ field, we
-- construct values of our data types above, and set each field to the name of
-- the column that it maps to:
--
-- First, @authorSchema@ describes the column names of the @author@ table when
-- associated with the @Author@ type:
--
-- >>> :{
-- authorSchema :: TableSchema (Author ColumnSchema)
-- authorSchema = TableSchema
-- { tableName = "author"
-- , tableSchema = Nothing
-- , tableColumns = Author
-- { authorId = "author_id"
-- , authorName = "name"
-- , authorUrl = "url"
-- }
-- }
-- :}
--
-- And likewise for @project@ and @Project@:
--
-- >>> :{
-- projectSchema :: TableSchema (Project ColumnSchema)
-- projectSchema = TableSchema
-- { tableName = "project"
-- , tableSchema = Nothing
-- , tableColumns = Project
-- { projectAuthorId = "author_id"
-- , projectName = "name"
-- }
-- }
-- :}
--
-- Aside: you might be wondering why this information isn't in the definitions
-- of @Author@ and @Project@ above. Rel8 decouples @TableSchema@ from the data
-- types themselves, as not all tables you define will necessarily have a
-- schema. For example, Rel8 allows you to define helper types to simplify the
-- types of queries - these tables only exist at query time, but there is no
-- corresponding base table. We'll see more on this idea later!
--
-- With these table definitions, we can now start writing some queries! To
-- actually run a query, we'll need a database connection. Rel8 uses the
-- @hasql@ library, and for this documentation we'll get the connection string
-- from the @$TEST_DATABASE_URL@ environment variable.
--
-- >>> connectionString <- System.Environment.getEnv "TEST_DATABASE_URL"
-- >>> Right c <- Hasql.Connection.acquire (Data.ByteString.Char8.pack connectionString)
-- >>> Control.Monad.void $ Hasql.Session.run (Hasql.Session.sql "BEGIN") c
-- $guideQueries
--
-- First, we'll take a look at @SELECT@ statements - usually the bulk of most
-- database heavy applications.
--
-- In Rel8, @SELECT@ statements are built using the 'Query' monad. You can
-- think of this monad like the ordinary @[]@ (List) monad - but this isn't
-- required knowledge.
--
-- To start, we'll look at one of the simplest queries possible - a basic
-- @SELECT * FROM@ statement. To select all rows from a table, we use 'each',
-- and supply a 'TableSchema'. So to select all @project@ rows, we can write:
--
-- >>> :t each projectSchema
-- each projectSchema :: Query (Project Expr)
--
-- Notice that @each@ gives us a @Query@ that yields @Project Expr@ rows. To
-- see what this means, let's have a look at a single field of a
-- @Project Expr@:
--
-- >>> let aProjectExpr = undefined :: Project Expr
-- >>> :t projectAuthorId aProjectExpr
-- projectAuthorId aProjectExpr :: Expr AuthorId
--
-- Recall we defined @projectAuthorId@ as @Column f AuthorId@. Now we have @f@
-- is @Expr@, and @Column Expr AuthorId@ reduces to @Expr AuthorId@. We'll see
-- more about @Expr@ soon, but you can think of @Expr a@ as "SQL expressions of
-- type @a@".
--
-- To execute this @Query@, we pass it to 'select':
--
-- >>> :t select c (each projectSchema)
-- select c (each projectSchema) :: MonadIO m => m [Project Identity]
--
-- When we @select@ things containing @Expr@s, Rel8 builds a new response table
-- with the @Identity@ interpretation. This means you'll get back plain Haskell
-- values. Studying @projectAuthorId@ again, we have:
--
-- >>> let aProjectIdentity = undefined :: Project Identity
-- >>> :t projectAuthorId aProjectIdentity
-- projectAuthorId aProjectIdentity :: AuthorId
--
-- Here @Column Identity AuthorId@ reduces to just @AuthorId@, with no
-- wrappping type at all.
--
-- Putting this all together, we can run our first query:
--
-- >>> select c (each projectSchema) >>= mapM_ print
-- Project {projectAuthorId = 1, projectName = "rel8"}
-- Project {projectAuthorId = 2, projectName = "aeson"}
-- Project {projectAuthorId = 2, projectName = "text"}
--
-- Cool!
--
-- We now know that 'each' is the equivalent of a @SELECT *@ query, but
-- sometimes we're only interested in a subset of the columns of a table. To
-- restrict the returned columns, we can specify a projection by using 'Query's
-- @Functor@ instance:
--
-- >>> select c $ projectName <$> each projectSchema
-- ["rel8","aeson","text"]
-- $guideJoins
--
-- Another common operation in relational databases is to take the @JOIN@ of
-- multiple tables. Rel8 doesn't have a specific join operation, but we can
-- recover the functionality of a join by selecting all rows of two tables, and
-- then using 'where_' to filter them.
--
-- To see how this works, first let's look at taking the product of two tables.
-- We can do this by simply calling 'each' twice, and then returning a tuple of
-- their results.
--
-- >>> :{
-- mapM_ print =<< select c do
-- author <- each authorSchema
-- project <- each projectSchema
-- return (projectName project, authorName author)
-- :}
-- ("rel8","Ollie")
-- ("rel8","Bryan O'Sullivan")
-- ("rel8","Emily Pillmore")
-- ("aeson","Ollie")
-- ("aeson","Bryan O'Sullivan")
-- ("aeson","Emily Pillmore")
-- ("text","Ollie")
-- ("text","Bryan O'Sullivan")
-- ("text","Emily Pillmore")
--
-- This isn't quite right, though, as we have ended up pairing up the wrong
-- projects and authors. To fix this, we can use 'where_' to restrict the
-- returned rows. We could write:
--
-- > select c $ do
-- > author <- each authorSchema
-- > project <- each projectSchema
-- > where_ $ projectAuthorId project ==. authorId author
-- > return (project, author)
--
-- but doing this every time you need a join can obscure the meaning of the
-- query you're writing. A good practice is to introduce specialised functions
-- for the particular joins in your database. In our case, this would be:
--
-- >>> :{
-- projectsForAuthor :: Author Expr -> Query (Project Expr)
-- projectsForAuthor a = each projectSchema >>= filter \p ->
-- projectAuthorId p ==. authorId a
-- :}
--
-- Our final query is then:
--
-- >>> :{
-- mapM_ print =<< select c do
-- author <- each authorSchema
-- project <- projectsForAuthor author
-- return (projectName project, authorName author)
-- :}
-- ("rel8","Ollie")
-- ("aeson","Bryan O'Sullivan")
-- ("text","Bryan O'Sullivan")
--
-- == @LEFT JOIN@s
--
-- Rel8 is also capable of performing @LEFT JOIN@s. To perform @LEFT JOIN@s, we
-- follow the same approach as before, but use the 'optional' query transformer
-- to allow for the possibility of the join to fail.
--
-- In our test database, we can see that there's another author who we haven't
-- seen yet:
--
-- >>> select c $ authorName <$> each authorSchema
-- ["Ollie","Bryan O'Sullivan","Emily Pillmore"]
--
-- Emily wasn't returned in our earlier query because - in our database - she
-- doesn't have any registered projects. We can account for this partiality in
-- our original query by wrapping the @projectsForAuthor@ call with 'optional':
--
-- >>> :{
-- mapM_ print =<< select c do
-- author <- each authorSchema
-- mproject <- optional $ projectsForAuthor author
-- return (authorName author, projectName <$> mproject)
-- :}
-- ("Ollie",Just "rel8")
-- ("Bryan O'Sullivan",Just "aeson")
-- ("Bryan O'Sullivan",Just "text")
-- ("Emily Pillmore",Nothing)
-- $guideAggregation
--
-- Aggregations are operations like @sum@ and @count@ - operations that reduce
-- multiple rows to single values. To perform aggregations in Rel8, we can use
-- the 'aggregate' function, which takes a 'Query' of aggregated expressions,
-- runs the aggregation, and returns aggregated rows.
--
-- To start, let's look at a simple aggregation that tells us how many projects
-- exist:
--
-- >>> error "TODO"
--
-- Rel8 is also capable of aggregating multiple rows into a single row by
-- concatenating all rows as a list. This aggregation allows us to break free
-- of the row-orientated nature of SQL and write queries that return tree-like
-- structures. Earlier we saw an example of returning authors with their
-- projects, but the query didn't do a great job of describing the one-to-many
-- relationship between authors and their projects.
--
-- Let's look again at a query that returns authors and their projects, and
-- focus on the /type/ of that query.
--
-- >>> :{
-- projectsForAuthor a = each projectSchema >>= filter \p ->
-- projectAuthorId p ==. authorId a
-- :}
--
-- >>> :{
-- let authorsAndProjects = do
-- author <- each authorSchema
-- project <- projectsForAuthor author
-- return (author, project)
-- where
-- :}
--
-- >>> :t select c authorsAndProjects
-- select c authorsAndProjects
-- :: MonadIO m => m [(Author Identity, Project Identity)]
--
-- Our query gives us a single list of pairs of authors and projects. However,
-- with our domain knowledge of the schema, this isn't a great type - what we'd
-- rather have is a list of pairs of authors and /lists/ of projects. That is,
-- what we'd like is:
--
-- > [(Author Identity, [Project Identity])]
--
-- This would be a much better type! Rel8 can produce a query with this type by
-- simply wrapping the call to @projectsForAuthor@ with either 'some' or
-- 'many'. Here we'll use 'many', which allows for the possibility of an author
-- to have no projects:
--
-- >>> :{
-- mapM_ print =<< select c do
-- author <- each authorSchema
-- projectNames <- many $ projectName <$> projectsForAuthor author
-- return (authorName author, projectNames)
-- :}
-- ("Ollie",["rel8"])
-- ("Bryan O'Sullivan",["aeson","text"])
-- ("Emily Pillmore",[])