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