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