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