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