on master help sign in
Sketch out the top-level Rel8 module
commit93168afc872c3af1a17ff32f10795607fe978a89 authorOllie Charles <ollie@ocharles.org.uk> dateTue 23 Mar 2021, 14:33 (6 years ago) parent37c305d
2 files changed+647−1
rel8.cabal+3−1 src/Rel8.hs+644
rel8.cabal+3−1
@@ -42,6 +42,9 @@ library
4242 hs-source-dirs:
4343 src
4444 exposed-modules:
45+ Rel8
46+
47+ other-modules:
4548 Rel8.Aggregate
4649
4750 Rel8.Expr
@@ -157,7 +160,6 @@ library
157160 Rel8.Type.Sum
158161 Rel8.Type.Tag
159162
160− other-modules:
161163 Rel8.Opaque
162164 Rel8.Schema.Structure
163165
src/Rel8.hsnew file+644
@@ -0,0 +1,644 @@
1+{-# language DuplicateRecordFields #-}
2+
3+module 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
17+ -- ** @DBType@
18+ DBType(..)
19+
20+ -- *** Deriving-via helpers
21+ -- **** @JSONEncoded@
22+ -- , JSONEncoded(..)
23+ -- , JSONBEncoded(..)
24+
25+ -- **** @ReadShow@
26+ -- , ReadShow(..)
27+
28+ -- *** @TypeInformation@
29+ , TypeInformation(..)
30+ , mapTypeInformation
31+ , parseTypeInformation
32+
33+ -- ** The @DBType@ hierarchy
34+ , DBSemigroup(..)
35+ , DBMonoid(..)
36+ , DBNum
37+ , DBFractional
38+
39+ -- * Tables and higher-kinded tables
40+ , Table
41+ , AltTable((<|>:))
42+ , EqTable, (==:), (/=:)
43+ , OrdTable, ascTable, descTable
44+ , lit
45+ , bool
46+ , case_
47+
48+ -- ** @MaybeTable@
49+ , MaybeTable(..)
50+ , maybeTable, nothingTable, justTable
51+ , isNothingTable, isJustTable
52+ , optional
53+ , catMaybeTable
54+ , bindMaybeTable
55+ , traverseMaybeTable
56+
57+ -- ** @EitherTable@
58+ , EitherTable
59+ , eitherTable, leftTable, rightTable
60+ , isLeftTable, isRightTable
61+ , keepLeftTable
62+ , keepRightTable
63+ , bindEitherTable
64+ , bitraverseEitherTable
65+
66+ -- ** @TheseTable@
67+ , TheseTable
68+ , theseTable, thisTable, thatTable, thoseTable
69+ , isThisTable, isThatTable, isThoseTable
70+ , hasHereTable, hasThereTable
71+ , justHereTable, justThereTable
72+ , alignBy
73+ , keepHereTable, loseHereTable
74+ , keepThereTable, loseThereTable
75+ , keepThisTable, loseThisTable
76+ , keepThatTable, loseThatTable
77+ , keepThoseTable, loseThoseTable
78+ , bindTheseTable
79+ , bitraverseTheseTable
80+
81+ -- ** @ListTable@
82+ , ListTable
83+ , many
84+
85+ -- ** @NonEmptyTable@
86+ , NonEmptyTable
87+ , some
88+
89+ -- ** Table schemas
90+ , TableSchema(..)
91+
92+ -- * Expressions
93+ , Expr
94+ , litExpr
95+
96+ -- ** @null@
97+ , nullify
98+ , nullable
99+ , isNull
100+ , isNonNull
101+ , mapNullable
102+ , catNullable
103+
104+ -- ** Boolean operations
105+ , true, false, not_
106+ , (&&.), and_
107+ , (||.), or_
108+ , (==.), (/=.), (==?), (/=?)
109+ , in_
110+ , boolExpr, caseExpr
111+
112+ -- ** Ordering
113+ , DBOrd
114+ , (<.), (<=.), (>.), (>=.)
115+ , (<?), (<=?), (>?), (>=?)
116+ , leastExpr, greatestExpr
117+
118+ -- ** Functions
119+ , Function
120+ , function
121+ , nullaryFunction
122+
123+ -- * Queries
124+ , Query
125+ , showQuery
126+
127+ -- ** Selecting rows
128+ , Selects
129+ , each
130+ , values
131+
132+ -- ** Filtering
133+ , filter
134+ , where_
135+ , whereExists
136+ , whereNotExists
137+ , distinct
138+ , distinctOn
139+ , distinctOnBy
140+
141+ -- ** @LIMIT@/@OFFSET@
142+ , limit
143+ , offset
144+
145+ -- ** @UNION@
146+ , union
147+ , unionAll
148+
149+ -- ** @INTERSECT@
150+ , intersect
151+ , intersectAll
152+
153+ -- ** @EXCEPT@
154+ , except
155+ , exceptAll
156+
157+ -- ** @EXISTS@
158+ , exists
159+ , with
160+ , withBy
161+ , without
162+ , withoutBy
163+
164+ -- ** Aggregation
165+ , Aggregate
166+ , aggregate
167+ , listAgg
168+ , nonEmptyAgg
169+ , groupBy
170+ , DBMax, max
171+ , DBMin, min
172+ , DBSum, sum
173+ , DBString, stringAgg
174+ , count
175+ , countStar
176+ , countDistinct
177+ , countWhere
178+ , and
179+ , or
180+
181+ -- ** Ordering
182+ , orderBy
183+ , Order
184+ , asc
185+ , desc
186+ , nullsFirst
187+ , nullsLast
188+
189+ -- * IO
190+ , Serializable
191+
192+ -- * Running statements
193+ -- ** @SELECT@
194+ , select
195+
196+ -- ** @INSERT@
197+ , Insert(..)
198+ , OnConflict(..)
199+ , insert
200+ , toInsert
201+ , toInsertDefaults
202+
203+ -- ** @DELETE@
204+ , Delete(..)
205+ , delete
206+
207+ -- ** @UPDATE@
208+ , update
209+ , Update(..)
210+
211+ -- ** @.. RETURNING@
212+ , Returning(..)
213+
214+ -- * TODO
215+ -- TODO
216+ -- These need organizing, but are reachable from Rel8's documentation so we
217+ -- do need to export and document them.
218+ , Nullabilizes
219+ , HTable
220+ , Labelable
221+ , Context
222+ ) where
223+
224+-- base
225+import Prelude ()
226+
227+-- rel8
228+import Rel8.Type
229+import Rel8.Type.Information
230+import Rel8.Type.Ord
231+import Rel8.Type.Semigroup
232+import Rel8.Type.Monoid
233+import Rel8.Type.Num
234+import Rel8.Table
235+import Rel8.Expr
236+import Rel8.Expr.Bool
237+import Rel8.Expr.Eq
238+import Rel8.Expr.Function
239+import Rel8.Query
240+import Rel8.Query.SQL (showQuery)
241+import Rel8.Query.Each
242+import Rel8.Query.Values
243+import Rel8.Query.Filter
244+import Rel8.Query.Exists
245+import Rel8.Query.Distinct
246+import Rel8.Query.Limit
247+import Rel8.Query.Set
248+import Rel8.Table.Maybe
249+import Rel8.Query.Maybe
250+import Rel8.Aggregate
251+import Rel8.Query.Aggregate
252+import Rel8.Query.List
253+import Rel8.Table.NonEmpty
254+import Rel8.Table.List
255+import Rel8.Query.Order
256+import Rel8.Order
257+import Rel8.Table.Serialize
258+import Rel8.Statement.Select
259+import Rel8.Statement.Insert
260+import Rel8.Statement.Delete
261+import Rel8.Statement.Returning
262+import Rel8.Statement.Update
263+import Rel8.Table.Aggregate
264+import Rel8.Expr.Aggregate
265+import Rel8.Type.String
266+import Rel8.Type.Sum
267+import Rel8.Expr.Null
268+import Rel8.Query.Null
269+import Rel8.Expr.Order
270+import Rel8.Schema.Nullability
271+import Rel8.Schema.HTable
272+import Rel8.Schema.Context.Label
273+import Rel8.Schema.Table
274+import Rel8.Table.These
275+import Rel8.Query.These
276+import Rel8.Table.Recontextualize
277+import Rel8.Expr.Ord
278+import Rel8.Table.Alternative
279+import Rel8.Table.Either
280+import Rel8.Table.Bool
281+import Rel8.Query.Either
282+import Rel8.Table.Eq
283+import Rel8.Table.Ord
284+import Rel8.Table.Order
285+import Rel8.Expr.Serialize
286+import Rel8.Table.Insert
287+
288+
289+-- $setup
290+--
291+-- In this section, we'll take a look at using Rel8 to work with a small
292+-- database for Haskell packages. We'll take a look at idiomatic usage of Rel8,
293+-- mapping tables to Haskell, and then look at writing some simple queries.
294+--
295+-- The documentation in this library uses the following language extensions:
296+--
297+-- >>> :set -XBlockArguments
298+-- >>> :set -XDeriveAnyClass
299+-- >>> :set -XDeriveGeneric
300+-- >>> :set -XDerivingStrategies
301+-- >>> :set -XDerivingVia
302+-- >>> :set -XDuplicateRecordFields
303+-- >>> :set -XGeneralizedNewtypeDeriving
304+-- >>> :set -XOverloadedStrings
305+-- >>> :set -XStandaloneDeriving
306+-- >>> :set -XTypeApplications
307+-- >>> :set -XTypeFamilies
308+--
309+-- Before we start writing any Haskell, let's take a look at the schema we'll
310+-- work with.
311+--
312+-- > author: project:
313+-- >
314+-- > Column │ Type │ Nullable Column │ Type │ Nullable
315+-- > ═══════════╪═════════╪══════════ ═══════════╪═════════╪══════════
316+-- > author_id │ integer │ not null author_id │ integer │ not null
317+-- > name │ text │ not null name │ text │ not null
318+-- > url │ text │
319+--
320+-- Our schema consists of two tables - @author@ and @project@. A @project@
321+-- always has an @author@, but not all @author@s have projects. Each @author@
322+-- has a name and (maybe) an associated website, and each project has a name.
323+--
324+-- Now that we've seen our schema, we can begin writing a mapping in Rel8. The
325+-- idiomatic way to map a table is to use a record that is parameterised what
326+-- Rel8 calls an /interpretation functor/, and to define each field with
327+-- 'Column'. For this type to be usable with Rel8 we need it to be an instance
328+-- of 'HigherKindedTable', which can be derived with a combination of
329+-- @DeriveAnyClass@ and @DeriveGeneric@.
330+--
331+-- Following these steps for @author@, we have:
332+--
333+-- > data Author f = Author
334+-- > { authorId :: Column f Int64
335+-- > , name :: Column f Text
336+-- > , url :: Column f (Maybe Text)
337+-- > } deriving (Generic, HigherKindedTable)
338+--
339+-- This is a perfectly reasonable definition, but cautious readers might notice
340+-- a problem - in particular, with the type of the @authorId@ field. While
341+-- @Int64@ is correct, it's not the best type. If we had other identifier types
342+-- in our project, it would be too easy to accidentally mix them up and create
343+-- nonsensical joins. As Haskell programmers, we often solve this problem by
344+-- creating @newtype@ wrappers, and we can also use this technique with Rel8:
345+--
346+-- >>> :{
347+-- newtype AuthorId = AuthorId { toInt64 :: Int64 }
348+-- deriving newtype (DBEq, DBType, Eq, Show)
349+-- :}
350+--
351+-- Now we can write our final schema mapping. First, the @author@ table:
352+--
353+-- >>> :{
354+-- data Author f = Author
355+-- { authorId :: Column f AuthorId
356+-- , authorName :: Column f Text
357+-- , authorUrl :: Column f (Maybe Text)
358+-- }
359+-- deriving stock Generic
360+-- deriving anyclass HigherKindedTable
361+-- :}
362+--
363+-- And similarly, the @project@ table:
364+--
365+-- >>> :{
366+-- data Project f = Project
367+-- { projectAuthorId :: Column f AuthorId
368+-- , projectName :: Column f Text
369+-- }
370+-- deriving stock Generic
371+-- deriving anyclass HigherKindedTable
372+-- :}
373+--
374+-- To show query results in this documentation, we'll also need @Show@
375+-- instances: Unfortunately these definitions look a bit scary, but they are
376+-- essentially just @deriving (Show)@:
377+--
378+-- >>> deriving stock instance f ~ Identity => Show (Author f)
379+-- >>> deriving stock instance f ~ Identity => Show (Project f)
380+--
381+-- These data types describe the structural mapping of the tables, but we also
382+-- need to specify a 'TableSchema' for each table. A @TableSchema@ contains the
383+-- name of the table and the name of all columns in the table, which will
384+-- ultimately allow us to @SELECT@ and @INSERT@ rows for these tables.
385+--
386+-- To define a @TableSchema@, we just need to fill construct appropriate
387+-- @TableSchema@ values. When it comes to the @tableColumns@ field, we
388+-- construct values of our data types above, and set each field to the name of
389+-- the column that it maps to:
390+--
391+-- First, @authorSchema@ describes the column names of the @author@ table when
392+-- associated with the @Author@ type:
393+--
394+-- >>> :{
395+-- authorSchema :: TableSchema (Author ColumnSchema)
396+-- authorSchema = TableSchema
397+-- { tableName = "author"
398+-- , tableSchema = Nothing
399+-- , tableColumns = Author
400+-- { authorId = "author_id"
401+-- , authorName = "name"
402+-- , authorUrl = "url"
403+-- }
404+-- }
405+-- :}
406+--
407+-- And likewise for @project@ and @Project@:
408+--
409+-- >>> :{
410+-- projectSchema :: TableSchema (Project ColumnSchema)
411+-- projectSchema = TableSchema
412+-- { tableName = "project"
413+-- , tableSchema = Nothing
414+-- , tableColumns = Project
415+-- { projectAuthorId = "author_id"
416+-- , projectName = "name"
417+-- }
418+-- }
419+-- :}
420+--
421+-- Aside: you might be wondering why this information isn't in the definitions
422+-- of @Author@ and @Project@ above. Rel8 decouples @TableSchema@ from the data
423+-- types themselves, as not all tables you define will necessarily have a
424+-- schema. For example, Rel8 allows you to define helper types to simplify the
425+-- types of queries - these tables only exist at query time, but there is no
426+-- corresponding base table. We'll see more on this idea later!
427+--
428+-- With these table definitions, we can now start writing some queries! To
429+-- actually run a query, we'll need a database connection. Rel8 uses the
430+-- @hasql@ library, and for this documentation we'll get the connection string
431+-- from the @$TEST_DATABASE_URL@ environment variable.
432+--
433+-- >>> connectionString <- System.Environment.getEnv "TEST_DATABASE_URL"
434+-- >>> Right c <- Hasql.Connection.acquire (Data.ByteString.Char8.pack connectionString)
435+-- >>> Control.Monad.void $ Hasql.Session.run (Hasql.Session.sql "BEGIN") c
436+
437+-- $guideQueries
438+--
439+-- First, we'll take a look at @SELECT@ statements - usually the bulk of most
440+-- database heavy applications.
441+--
442+-- In Rel8, @SELECT@ statements are built using the 'Query' monad. You can
443+-- think of this monad like the ordinary @[]@ (List) monad - but this isn't
444+-- required knowledge.
445+--
446+-- To start, we'll look at one of the simplest queries possible - a basic
447+-- @SELECT * FROM@ statement. To select all rows from a table, we use 'each',
448+-- and supply a 'TableSchema'. So to select all @project@ rows, we can write:
449+--
450+-- >>> :t each projectSchema
451+-- each projectSchema :: Query (Project Expr)
452+--
453+-- Notice that @each@ gives us a @Query@ that yields @Project Expr@ rows. To
454+-- see what this means, let's have a look at a single field of a
455+-- @Project Expr@:
456+--
457+-- >>> let aProjectExpr = undefined :: Project Expr
458+-- >>> :t projectAuthorId aProjectExpr
459+-- projectAuthorId aProjectExpr :: Expr AuthorId
460+--
461+-- Recall we defined @projectAuthorId@ as @Column f AuthorId@. Now we have @f@
462+-- is @Expr@, and @Column Expr AuthorId@ reduces to @Expr AuthorId@. We'll see
463+-- more about @Expr@ soon, but you can think of @Expr a@ as "SQL expressions of
464+-- type @a@".
465+--
466+-- To execute this @Query@, we pass it to 'select':
467+--
468+-- >>> :t select c (each projectSchema)
469+-- select c (each projectSchema) :: MonadIO m => m [Project Identity]
470+--
471+-- When we @select@ things containing @Expr@s, Rel8 builds a new response table
472+-- with the @Identity@ interpretation. This means you'll get back plain Haskell
473+-- values. Studying @projectAuthorId@ again, we have:
474+--
475+-- >>> let aProjectIdentity = undefined :: Project Identity
476+-- >>> :t projectAuthorId aProjectIdentity
477+-- projectAuthorId aProjectIdentity :: AuthorId
478+--
479+-- Here @Column Identity AuthorId@ reduces to just @AuthorId@, with no
480+-- wrappping type at all.
481+--
482+-- Putting this all together, we can run our first query:
483+--
484+-- >>> select c (each projectSchema) >>= mapM_ print
485+-- Project {projectAuthorId = 1, projectName = "rel8"}
486+-- Project {projectAuthorId = 2, projectName = "aeson"}
487+-- Project {projectAuthorId = 2, projectName = "text"}
488+--
489+-- Cool!
490+--
491+-- We now know that 'each' is the equivalent of a @SELECT *@ query, but
492+-- sometimes we're only interested in a subset of the columns of a table. To
493+-- restrict the returned columns, we can specify a projection by using 'Query's
494+-- @Functor@ instance:
495+--
496+-- >>> select c $ projectName <$> each projectSchema
497+-- ["rel8","aeson","text"]
498+
499+-- $guideJoins
500+--
501+-- Another common operation in relational databases is to take the @JOIN@ of
502+-- multiple tables. Rel8 doesn't have a specific join operation, but we can
503+-- recover the functionality of a join by selecting all rows of two tables, and
504+-- then using 'where_' to filter them.
505+--
506+-- To see how this works, first let's look at taking the product of two tables.
507+-- We can do this by simply calling 'each' twice, and then returning a tuple of
508+-- their results.
509+--
510+-- >>> :{
511+-- mapM_ print =<< select c do
512+-- author <- each authorSchema
513+-- project <- each projectSchema
514+-- return (projectName project, authorName author)
515+-- :}
516+-- ("rel8","Ollie")
517+-- ("rel8","Bryan O'Sullivan")
518+-- ("rel8","Emily Pillmore")
519+-- ("aeson","Ollie")
520+-- ("aeson","Bryan O'Sullivan")
521+-- ("aeson","Emily Pillmore")
522+-- ("text","Ollie")
523+-- ("text","Bryan O'Sullivan")
524+-- ("text","Emily Pillmore")
525+--
526+-- This isn't quite right, though, as we have ended up pairing up the wrong
527+-- projects and authors. To fix this, we can use 'where_' to restrict the
528+-- returned rows. We could write:
529+--
530+-- > select c $ do
531+-- > author <- each authorSchema
532+-- > project <- each projectSchema
533+-- > where_ $ projectAuthorId project ==. authorId author
534+-- > return (project, author)
535+--
536+-- but doing this every time you need a join can obscure the meaning of the
537+-- query you're writing. A good practice is to introduce specialised functions
538+-- for the particular joins in your database. In our case, this would be:
539+--
540+-- >>> :{
541+-- projectsForAuthor :: Author Expr -> Query (Project Expr)
542+-- projectsForAuthor a = each projectSchema >>= filter \p ->
543+-- projectAuthorId p ==. authorId a
544+-- :}
545+--
546+-- Our final query is then:
547+--
548+-- >>> :{
549+-- mapM_ print =<< select c do
550+-- author <- each authorSchema
551+-- project <- projectsForAuthor author
552+-- return (projectName project, authorName author)
553+-- :}
554+-- ("rel8","Ollie")
555+-- ("aeson","Bryan O'Sullivan")
556+-- ("text","Bryan O'Sullivan")
557+--
558+-- == @LEFT JOIN@s
559+--
560+-- Rel8 is also capable of performing @LEFT JOIN@s. To perform @LEFT JOIN@s, we
561+-- follow the same approach as before, but use the 'optional' query transformer
562+-- to allow for the possibility of the join to fail.
563+--
564+-- In our test database, we can see that there's another author who we haven't
565+-- seen yet:
566+--
567+-- >>> select c $ authorName <$> each authorSchema
568+-- ["Ollie","Bryan O'Sullivan","Emily Pillmore"]
569+--
570+-- Emily wasn't returned in our earlier query because - in our database - she
571+-- doesn't have any registered projects. We can account for this partiality in
572+-- our original query by wrapping the @projectsForAuthor@ call with 'optional':
573+--
574+-- >>> :{
575+-- mapM_ print =<< select c do
576+-- author <- each authorSchema
577+-- mproject <- optional $ projectsForAuthor author
578+-- return (authorName author, projectName <$> mproject)
579+-- :}
580+-- ("Ollie",Just "rel8")
581+-- ("Bryan O'Sullivan",Just "aeson")
582+-- ("Bryan O'Sullivan",Just "text")
583+-- ("Emily Pillmore",Nothing)
584+
585+-- $guideAggregation
586+--
587+-- Aggregations are operations like @sum@ and @count@ - operations that reduce
588+-- multiple rows to single values. To perform aggregations in Rel8, we can use
589+-- the 'aggregate' function, which takes a 'Query' of aggregated expressions,
590+-- runs the aggregation, and returns aggregated rows.
591+--
592+-- To start, let's look at a simple aggregation that tells us how many projects
593+-- exist:
594+--
595+-- >>> error "TODO"
596+--
597+-- Rel8 is also capable of aggregating multiple rows into a single row by
598+-- concatenating all rows as a list. This aggregation allows us to break free
599+-- of the row-orientated nature of SQL and write queries that return tree-like
600+-- structures. Earlier we saw an example of returning authors with their
601+-- projects, but the query didn't do a great job of describing the one-to-many
602+-- relationship between authors and their projects.
603+--
604+-- Let's look again at a query that returns authors and their projects, and
605+-- focus on the /type/ of that query.
606+--
607+-- >>> :{
608+-- projectsForAuthor a = each projectSchema >>= filter \p ->
609+-- projectAuthorId p ==. authorId a
610+-- :}
611+--
612+-- >>> :{
613+-- let authorsAndProjects = do
614+-- author <- each authorSchema
615+-- project <- projectsForAuthor author
616+-- return (author, project)
617+-- where
618+-- :}
619+--
620+-- >>> :t select c authorsAndProjects
621+-- select c authorsAndProjects
622+-- :: MonadIO m => m [(Author Identity, Project Identity)]
623+--
624+-- Our query gives us a single list of pairs of authors and projects. However,
625+-- with our domain knowledge of the schema, this isn't a great type - what we'd
626+-- rather have is a list of pairs of authors and /lists/ of projects. That is,
627+-- what we'd like is:
628+--
629+-- > [(Author Identity, [Project Identity])]
630+--
631+-- This would be a much better type! Rel8 can produce a query with this type by
632+-- simply wrapping the call to @projectsForAuthor@ with either 'some' or
633+-- 'many'. Here we'll use 'many', which allows for the possibility of an author
634+-- to have no projects:
635+--
636+-- >>> :{
637+-- mapM_ print =<< select c do
638+-- author <- each authorSchema
639+-- projectNames <- many $ projectName <$> projectsForAuthor author
640+-- return (authorName author, projectNames)
641+-- :}
642+-- ("Ollie",["rel8"])
643+-- ("Bryan O'Sullivan",["aeson","text"])
644+-- ("Emily Pillmore",[])